baseline: Clover 独立仓库首次基线提交

将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-16 11:30:22 +08:00
commit 6a2632da70
253 changed files with 27467 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
"""
app/workers/pipeline_steps.py — 生产链 Step1-4
Step1: 查 DBtask/product
Step2: 查 key → Fernet 解密局部变量不传出基石B
Step3: 构建 AI clients
Step4: 推 task_started SSE + 飞轮上下文
"""
import json
import logging
logger = logging.getLogger(__name__)
def load_task_and_product(db, task_id: int):
"""Step1: 查任务 + 产品,失败返 None 或抛异常。"""
from app.models.task import GenerationTask
from app.models.product import Product
task = db.query(GenerationTask).filter(GenerationTask.id == task_id).first()
if not task:
logger.error("task_id=%s not found", task_id)
return None, None
product = db.query(Product).filter(Product.id == task.product_id).first()
if not product:
raise ValueError(f"product_id={task.product_id} not found")
return task, product
def decrypt_user_key(db, operator_id: int, workspace_id: int) -> str:
"""
Step2: 查 key → Fernet 解密,返回 plain_key只活在调用方局部变量
绝不打印、不持久化 plain_key基石B
"""
from app.models.workspace import UserApiKey
from app.utils.fernet_utils import decrypt_key
api_key_row = db.query(UserApiKey).filter(
UserApiKey.user_id == operator_id,
UserApiKey.workspace_id == workspace_id,
UserApiKey.provider.in_(["openai", "apiports"]), # G6坑修复接受主备通道名
).first()
if not api_key_row:
raise ValueError("用户未配置 API Key请先录入")
return decrypt_key(api_key_row.encrypted_key)
def build_clients_and_clear_key(plain_key: str):
"""
Step3: 构建 AIClientsplain_key 传入后立即由调用方置 None。
返回 clients 对象。
"""
from app.services.ai_engine.gemini_factory import build_ai_clients
return build_ai_clients(plain_key)
def build_product_dict(product) -> dict:
"""把 ORM product 转成 AI 引擎所需的 dict不含任何 key"""
return {
"name": product.name,
"category": product.category or "通用好物",
"selling_points": json.loads(product.selling_points or "[]"),
"style_tone": product.style_tone or "素人分享风",
"text_angles": json.loads(product.text_angles or "[]"),
"custom_prompt": product.custom_prompt or "",
"brand_keyword": product.brand_keyword or "", # S3: 品牌词透传进生成prompt(每条植入)
"target_audience": product.target_audience or "", # 012: 人群透传进storyboard/文案prompt
"image_path": product.image_path or "", # 产品参考图路径(前端上传后填入)
}
def load_flywheel_context(db, workspace_id: int, product_id: int, product_dict: dict) -> tuple[str, dict]:
"""
查最近50条飞轮事件聚合偏好上下文。
返回 (prompt_fragment, full_ctx)。
"""
from app.models.flywheel import PreferenceEvent
from app.services.ai_engine.preference_aggregator import aggregate_preference_context
recent = db.query(PreferenceEvent).filter(
PreferenceEvent.workspace_id == workspace_id,
PreferenceEvent.product_id == product_id,
).order_by(PreferenceEvent.id.desc()).limit(50).all()
events_dicts = [
{"signal_type": e.signal_type, "workspace_id": e.workspace_id,
"product_id": e.product_id, "angle_label": e.angle_label or "",
"signal_weight": e.signal_weight, "reason": e.reason or ""}
for e in recent
]
ctx = aggregate_preference_context(events_dicts, product_dict, workspace_id, product_id)
return ctx.get("prompt_fragment", ""), ctx