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:
yuzhiran
2026-07-11 12:36:01 +08:00
parent 3d596275d5
commit 7e953afbd2
39 changed files with 2418 additions and 311 deletions
+83 -33
View File
@@ -58,6 +58,7 @@ MODULES = {
"scheduled_task_monitor": {"name": "⏰ 任务监控", "cron": "*"},
"scheduled_rank_tracker": {"name": "🔍 搜索排名追踪", "cron": "07:00"},
"scheduled_geo_tracker": {"name": "🌐 AI 搜索引用追踪", "cron": "07:30"},
"scheduled_shortvideo": {"name": "🎬 短视频拆条", "cron": "08:00"},
}
LOG_FILE_MAP = {
@@ -71,6 +72,7 @@ LOG_FILE_MAP = {
"scheduled_task_monitor": "task_monitor",
"scheduled_rank_tracker": "rank_tracker",
"scheduled_geo_tracker": "geo_tracker",
"scheduled_shortvideo": "shortvideo",
}
def _log_to_file(module_id: str, status: str, message: str = None, error_trace: str = None):
@@ -188,6 +190,7 @@ class TaskScheduler:
("scheduled_reset_search_usage", self._run_reset_search_usage, "搜索用量重置"),
("scheduled_rank_tracker", self._run_rank_tracker, "搜索排名追踪"),
("scheduled_geo_tracker", self._run_geo_tracker, "AI 搜索引用追踪"),
("scheduled_shortvideo", self._run_shortvideo, "短视频拆条"),
]
for module_id, fn, name in MODULE_JOBS:
@@ -418,41 +421,48 @@ class TaskScheduler:
count = 0
for topic in topics:
platform_urls = topic.platform_urls or {}
zhihu_url = platform_urls.get("zhihu", "")
if not zhihu_url:
continue
m = re.search(r'zhuanlan\.zhihu\.com/p/(\d+)', zhihu_url)
if not m:
continue
post_id = m.group(1)
api_url = f"https://zhuanlan.zhihu.com/api/posts/{post_id}"
try:
resp = http_requests.get(api_url, headers={"User-Agent": ua}, timeout=10)
if resp.status_code != 200:
for platform, url in platform_urls.items():
if not url:
continue
if platform == "zhihu":
m = re.search(r'zhuanlan\.zhihu\.com/p/(\d+)', url)
if not m:
continue
post_id = m.group(1)
api_url = f"https://zhuanlan.zhihu.com/api/posts/{post_id}"
try:
resp = http_requests.get(api_url, headers={"User-Agent": ua}, timeout=10)
if resp.status_code != 200:
continue
raw = resp.json()
existing = db.query(ContentMetrics).filter(
ContentMetrics.topic_id == topic.id,
ContentMetrics.platform == "zhihu"
).first()
metric_data = {
"views": raw.get("voteup_count", raw.get("views_count", 0)),
"likes": raw.get("voteup_count", 0),
"favorites": raw.get("favorite_count", 0),
"comments": raw.get("comment_count", raw.get("comments_count", 0)),
"shares": raw.get("share_count", 0),
"last_fetched": datetime.now(),
"publish_url": url,
"data_snapshot": raw,
}
if existing:
for k, v in metric_data.items():
setattr(existing, k, v)
else:
db.add(ContentMetrics(topic_id=topic.id, platform="zhihu", **metric_data))
count += 1
except Exception:
continue
elif platform in ("wechat_mp", "xiaohongshu"):
logger.info("平台 %s 无公开指标 API,跳过", platform)
continue
raw = resp.json()
existing = db.query(ContentMetrics).filter(
ContentMetrics.topic_id == topic.id,
ContentMetrics.platform == "zhihu"
).first()
metric_data = {
"views": raw.get("voteup_count", raw.get("views_count", 0)),
"likes": raw.get("voteup_count", 0),
"favorites": raw.get("favorite_count", 0),
"comments": raw.get("comment_count", raw.get("comments_count", 0)),
"shares": raw.get("share_count", 0),
"last_fetched": datetime.now(),
"publish_url": zhihu_url,
"data_snapshot": raw,
}
if existing:
for k, v in metric_data.items():
setattr(existing, k, v)
else:
db.add(ContentMetrics(topic_id=topic.id, platform="zhihu", **metric_data))
count += 1
except Exception:
continue
logger.info("平台 %s 暂无指标同步支持,跳过", platform)
continue
if count:
db.commit()
logger.info("[Scheduled] Metrics sync completed: synced %d zhihu articles", count)
@@ -661,6 +671,46 @@ class TaskScheduler:
started_at=started, finished_at=datetime.now(timezone.utc))
logger.exception("[GeoTracker] AI 搜索引用追踪失败: %s", e)
def _run_shortvideo(self):
"""每日将前一日「待发布」选题的长文拆条为短视频脚本"""
started = datetime.now(timezone.utc)
log_id = _log_task("scheduled_shortvideo", "running", started_at=started)
try:
from ..database import SessionLocal
from ..models import Topic
db = SessionLocal()
try:
topics = db.query(Topic).filter(
Topic.status.in_(["ready", "待发布", "pending_publish"])
).order_by(Topic.ready_at.desc().nullslast()).limit(3).all()
finally:
db.close()
if not topics:
_log_task("scheduled_shortvideo", "success", log_id=log_id,
message="无待拆条选题", started_at=started, finished_at=datetime.now(timezone.utc))
return
import subprocess
total = 0
for t in topics:
res = subprocess.run(
[sys.executable, str(PROJECT_ROOT / "scripts" / "shortvideo.py"), "--topic-id", t.id, "--count", "3"],
capture_output=True, text=True, timeout=600
)
if res.returncode == 0:
total += 1
_log_task("scheduled_shortvideo", "success", log_id=log_id,
message=f"拆条完成: {total}/{len(topics)} 选题",
result_data={"topics": len(topics), "done": total},
started_at=started, finished_at=datetime.now(timezone.utc))
logger.info("[ShortVideo] 拆条完成: %d/%d", total, len(topics))
except Exception as e:
import traceback
_log_task("scheduled_shortvideo", "failed", log_id=log_id,
message=str(e),
error_trace=traceback.format_exc(),
started_at=started, finished_at=datetime.now(timezone.utc))
logger.exception("[ShortVideo] 拆条失败: %s", e)
def get_jobs(self):
"""返回当前所有定时任务的状态"""
jobs = []