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:
@@ -25,8 +25,7 @@ def _get_cmd(topic_ids: List[str] = None):
|
||||
cmd = [str(venv_python), str(script_path)] if venv_python.exists() else ["python3", str(script_path)]
|
||||
if topic_ids:
|
||||
cmd.extend(["--topic-ids", ','.join(topic_ids)])
|
||||
else:
|
||||
cmd.append("--today-only")
|
||||
# 不再使用 --today-only,避免三平台串行撰写时小红书文章错过审查窗口
|
||||
return cmd
|
||||
|
||||
def run_optimizer(topic_ids: List[str] = None):
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user