feat: 创作工作台统一 + AI味检测/白标 + 移动端补全 + 合规发布闭环
- 新增创作工作台 studio.html:合并选题/内容工厂/文章管理为单一 tab 入口(iframe embed 模式) - 新增 AI味检测模块(ai_slop API + 页面,合规软硬问题分级) - 新增白标品牌配置(branding API + 页面 + deploy 私有化交付包) - 发布闭环:publishing 放宽至 editor + records/mark-published 接口 - 移动端响应式补全(admin/calendar/ai-slop 表格卡片兜底) - 修复菜单幂等播种缺陷(按 path 对齐,避免功能页孤立) - 新增短视频脚本 shortvideo.py 与 2026 市场调研简报
This commit is contained in:
+72
-3
@@ -69,6 +69,33 @@ def _load_platform_config() -> dict:
|
||||
|
||||
PLATFORM_CONFIG = _load_platform_config()
|
||||
|
||||
_AI_DISCLOSURE_TEXT = {
|
||||
"zhihu": "本文由 AI 辅助创作,核心观点与数据经宇之然人工审核校对。",
|
||||
"wechat": "本文由 AI 辅助创作,核心观点与数据经宇之然人工审核校对。",
|
||||
"xiaohongshu": "✨ 本文由 AI 辅助生成,内容已人工核对~",
|
||||
}
|
||||
|
||||
def _load_ai_disclosure_enabled() -> bool:
|
||||
"""根据《人工智能生成合成内容标识办法》(2025-09-01 施行),AI 生成内容须主动声明。
|
||||
默认开启;可通过 SystemConfig(disclose_ai=false) 或环境变量 YZR_DISCLOSE_AI=0 关闭。"""
|
||||
try:
|
||||
if os.environ.get("YZR_DISCLOSE_AI", "").strip() == "0":
|
||||
return False
|
||||
from app.database import SessionLocal
|
||||
from app.models import SystemConfig
|
||||
db = SessionLocal()
|
||||
try:
|
||||
sc = db.query(SystemConfig).filter(SystemConfig.key == "disclose_ai").first()
|
||||
if sc and str(sc.value).strip().lower() in ("false", "0", "no"):
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
AI_DISCLOSURE_ENABLED = _load_ai_disclosure_enabled()
|
||||
|
||||
PLATFORM_NAMES = {
|
||||
"zhihu": "知乎专栏",
|
||||
"wechat": "微信公众号",
|
||||
@@ -235,10 +262,40 @@ def inject_geo_metadata(html: str, title: str, content: str, platform: str, tags
|
||||
return html
|
||||
|
||||
|
||||
def inject_ai_disclosure(html: str, platform: str) -> str:
|
||||
"""按《人工智能生成合成内容标识办法》注入 AI 生成声明。
|
||||
显式:文首/文末标注「AI 生成」;隐式:head 注入 ai-generated meta 与 AIGC 隐式标识字段。"""
|
||||
if not AI_DISCLOSURE_ENABLED:
|
||||
return html
|
||||
text = _AI_DISCLOSURE_TEXT.get(platform, _AI_DISCLOSURE_TEXT["zhihu"])
|
||||
|
||||
disclosure_block = f'\n<blockquote class="ai-disclosure">🤖 {text}</blockquote>\n'
|
||||
|
||||
# 文末声明(置于正文末尾,封面/互动钩子之前不影响阅读)
|
||||
if "<!-- AI_DISCLOSURE -->" in html:
|
||||
html = html.replace("<!-- AI_DISCLOSURE -->", disclosure_block.strip())
|
||||
else:
|
||||
# 在 </body> 前插入文末声明
|
||||
html = html.replace("</body>", f"{disclosure_block}</body>")
|
||||
|
||||
# 隐式标识:head 注入声明 meta + AIGC 元数据字段(符合标识办法附录 E)
|
||||
head_meta = (
|
||||
'<meta name="ai-generated" content="true">\n'
|
||||
'<meta name="ai-disclosure" content="本文含人工智能生成合成内容">\n'
|
||||
'<script type="application/ld+json">\n'
|
||||
'{"AIGC":{"Label":"1","ContentProducer":"宇之然","ProduceID":"yzr-' + platform + '",'
|
||||
'"ContentPropagator":"宇之然","PropagateID":"yzr-' + platform + '"}}\n'
|
||||
'</script>\n'
|
||||
)
|
||||
html = html.replace("</head>", head_meta + "</head>")
|
||||
return html
|
||||
|
||||
|
||||
class Writer:
|
||||
def __init__(self, topic_id: str):
|
||||
self.topic_id = topic_id
|
||||
self.topic = self._load_topic()
|
||||
self._degraded = False
|
||||
outline_file = OUTLINE_DIR / TODAY / f"{topic_id}_outline.md"
|
||||
if not outline_file.exists():
|
||||
raise FileNotFoundError(f"Outline not found: {outline_file}")
|
||||
@@ -334,6 +391,9 @@ class Writer:
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM 扩写失败 [{platform}]: {e}")
|
||||
|
||||
# LLM 不可用或返回为空 → 降级为大纲要点拼接(非真正成稿)
|
||||
self._degraded = True
|
||||
logger.warning(f"⚠️ [{platform}] 章节「{section['title']}」LLM 扩写缺失,降级为大纲要点拼接(内容未达发布质量)")
|
||||
lines = [l.strip() for l in content.split('\n') if not self._is_outline_noise(l)]
|
||||
if lines:
|
||||
sentences = []
|
||||
@@ -608,8 +668,11 @@ class Writer:
|
||||
html = html.replace("<!-- TAGS -->", tags_html)
|
||||
else:
|
||||
html = html.replace("<!-- TAGS -->", "")
|
||||
|
||||
html = inject_geo_metadata(html, title, adapted, platform, tags_html)
|
||||
|
||||
# 强制 AI 生成声明(合规要求,可配置关闭)
|
||||
html = inject_ai_disclosure(html, platform)
|
||||
|
||||
return html
|
||||
|
||||
def save_html(self, html: str, platform: str, *, title: str = "", content: str = "") -> str:
|
||||
@@ -634,8 +697,14 @@ class Writer:
|
||||
html = self.generate_platform_html(markdown, platform)
|
||||
results[platform] = str(self.save_html(html, platform, title=title, content=markdown))
|
||||
self.mark_draft()
|
||||
logger.info(f"撰写完成,状态已更新为待审查")
|
||||
return {"ok": True, "files": results}
|
||||
if self._degraded:
|
||||
logger.error(
|
||||
"❌ 撰写完成但存在 LLM 降级章节(大纲要点拼接,未达发布质量)。"
|
||||
"选题保留为待审查,请检查 LLM 提供商连通性后重跑,勿直接发布降级稿。"
|
||||
)
|
||||
else:
|
||||
logger.info(f"撰写完成,状态已更新为待审查")
|
||||
return {"ok": True, "files": results, "degraded": self._degraded}
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
Reference in New Issue
Block a user