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
+104 -14
View File
@@ -2,13 +2,13 @@
"""发布管理 API"""
from fastapi import APIRouter, HTTPException, Depends, Request
from pydantic import BaseModel
from datetime import datetime, timezone, timedelta
from datetime import datetime, timezone, timedelta, date
from typing import Optional, List
from ..database import get_db
from ..models import Topic, PublishRecord, User
from sqlalchemy.orm import Session
from .auth import get_current_admin, org_filter
from .auth import get_current_user, org_filter
from ..core.audit_logger import audit_log
router = APIRouter(prefix="/api/publishing", tags=["publishing"])
@@ -40,9 +40,9 @@ async def create_publish_record(
req: MultiPublishRequest,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin)
current_user: User = Depends(get_current_user)
):
"""多平台发布选题(管理员"""
"""多平台发布选题(运营/管理员,按组织隔离"""
try:
q = db.query(Topic).filter(Topic.id == req.topic_id)
of = org_filter(current_user, Topic)
@@ -66,17 +66,17 @@ async def create_publish_record(
record = PublishRecord(
topic_id=req.topic_id,
platform=platform,
action='publish',
status='success',
action='stage',
status='pending_manual',
operator=operator,
description=f"选题 {req.topic_id} 发布到 {PLATFORM_LABELS.get(platform, platform)}",
description=f"选题 {req.topic_id} 已生成 {PLATFORM_LABELS.get(platform, platform)} 发布稿,待人工校验后发布",
org_id=topic.org_id
)
db.add(record)
results.append(PublishResult(
platform=platform,
platform_label=PLATFORM_LABELS.get(platform, platform),
status='success'
status='pending_manual'
))
except Exception as e:
results.append(PublishResult(
@@ -86,16 +86,17 @@ async def create_publish_record(
error_msg=str(e)
))
topic.status = '已发布'
# 诚实状态流转:本项目无真实自动发布器(且按《AI生成内容标识办法》AI 稿需真人把关),
# 故仅将选题置为「待人工发布」,待运营在对应平台校验声明后手动发布。
topic.status = 'pending_publish'
_now = datetime.now(timezone(timedelta(hours=8)))
topic.updated_at = _now
topic.published_at = _now.date()
db.commit()
db.expire_all()
db.refresh(topic)
audit_log(
action="publish",
action="publish_stage",
user=current_user,
resource_type="topic",
resource_id=req.topic_id,
@@ -105,15 +106,104 @@ async def create_publish_record(
db=db
)
success_count = sum(1 for r in results if r.status == 'success')
staged = sum(1 for r in results if r.status == 'pending_manual')
return MultiPublishResponse(
ok=success_count > 0,
ok=staged == len(results),
topic_id=req.topic_id,
results=results,
message=f"选题 {req.topic_id} 发布完成({success_count}/{len(results)} 平台成功)"
message=(
f"选题 {req.topic_id} 已生成 {staged}/{len(results)} 个平台的发布稿并置为「待人工发布」。"
f"AI 生成内容须由真人校验声明后手动发布,系统不代发。"
)
)
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/records")
def list_publish_records(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""列出发布记录(按组织隔离),用于运营查看「待人工发布 / 已发布」状态"""
q = db.query(PublishRecord)
of = org_filter(current_user, PublishRecord)
if of is not True:
q = q.filter(of)
recs = q.order_by(PublishRecord.created_at.desc()).all()
result = []
for r in recs:
topic = db.query(Topic).filter(Topic.id == r.topic_id).first()
result.append({
"id": r.id,
"topic_id": r.topic_id,
"topic_title": topic.title if topic else None,
"platform": r.platform,
"action": r.action,
"status": r.status,
"operator": r.operator,
"description": r.description,
"url": r.url,
"created_at": r.created_at.isoformat() if r.created_at else None,
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
})
return {"records": result}
class MarkPublishedResponse(BaseModel):
ok: bool
topic_id: str
message: str
@router.post("/{topic_id}/mark-published", response_model=MarkPublishedResponse)
def mark_published(
topic_id: str,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""运营在对应平台手动发布后,回填「已发布」闭环状态"""
q = db.query(Topic).filter(Topic.id == topic_id)
of = org_filter(current_user, Topic)
if of is not True:
q = q.filter(of)
topic = q.first()
if not topic:
raise HTTPException(status_code=404, detail=f"选题 {topic_id} 不存在")
if topic.status != 'pending_publish':
raise HTTPException(status_code=400, detail=f"选题 {topic_id} 当前状态为「{topic.status}」,无需标记发布")
_now = datetime.now(timezone(timedelta(hours=8)))
topic.status = 'published'
topic.published_at = date.today()
topic.updated_at = _now
recs = db.query(PublishRecord).filter(
PublishRecord.topic_id == topic_id,
PublishRecord.status == 'pending_manual'
)
for r in recs:
r.status = 'published'
r.updated_at = _now
db.commit()
audit_log(
action="publish_confirm",
user=current_user,
resource_type="topic",
resource_id=topic_id,
details={"operator": current_user.username},
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return MarkPublishedResponse(
ok=True,
topic_id=topic_id,
message=f"选题 {topic_id} 已标记为「已发布」,发布记录同步更新。"
)