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,145 @@
"""
偏好飞轮聚合preference_aggregator
扒自Clover架构方案.md §偏好飞轮怎么转 + PRD §8
三层继承L1 公司品牌基线 > L2 矩阵号人设(二期)> L3 个人手感
聚合最近 FLYWHEEL_LOOKBACK 条 events → prompt 片段注入文案生成
关键:
- 按 product_id 分开学(素颜霜偏好不串精华)
- 信号不足 FLYWHEEL_COLD_START 条时,用产品档案冷启动
- 返回结构对齐 API契约 GET /tasks/{id}/preference/context
"""
from __future__ import annotations
import logging
from collections import Counter
from typing import Any
from .constants import FLYWHEEL_LOOKBACK, FLYWHEEL_COLD_START
logger = logging.getLogger(__name__)
def aggregate_preference_context(
events: list[dict],
product: dict,
workspace_id: int,
product_id: int,
) -> dict:
"""
输入:最近 preference_events 行(已按 workspace_id+product_id 过滤)
输出:{recent_preference, reject_reasons, injected_count, prompt_fragment}
prompt_fragment 直接注入文案生成 prompt
"""
# 按 product_id 过滤(防串货)
relevant = [
e for e in events
if e.get("workspace_id") == workspace_id and e.get("product_id") == product_id
][:FLYWHEEL_LOOKBACK]
injected_count = len(relevant)
if injected_count < FLYWHEEL_COLD_START:
# 冷启动:用产品档案静态基线
return _cold_start(product, injected_count)
# ── 统计最常选角度text_select + approve 信号)
angle_counts: Counter = Counter()
reject_reasons: list[str] = []
for e in relevant:
sig_type = e.get("signal_type", "")
angle = str(e.get("angle_label", "")).strip()
weight = int(e.get("signal_weight", 1))
if sig_type in ("text_select", "approve") and angle:
angle_counts[angle] += weight
elif sig_type == "reject_with_reason":
reason = str(e.get("reason", "")).strip()
if reason:
reject_reasons.append(reason)
# 取权重最高的角度
top_angles = [a for a, _ in angle_counts.most_common(3)]
# 取最近3条打回原因
recent_rejects = reject_reasons[-3:] if reject_reasons else []
# ── 拼 prompt 片段三层继承L1>L2>L3一期只跑L1+L3
prompt_fragment = _build_prompt_fragment(top_angles, recent_rejects, product)
# ── 人类可读摘要(前端"本次已注入"显示)
if top_angles:
pref_summary = f"最近偏好角度:{''.join(top_angles)}(已选{injected_count}次信号)"
else:
pref_summary = f"已注入{injected_count}条偏好信号"
return {
"recent_preference": pref_summary,
"reject_reasons": recent_rejects,
"injected_count": injected_count,
"prompt_fragment": prompt_fragment, # 注入 generate_text_variants extra_rules
}
def _cold_start(product: dict, injected_count: int) -> dict:
"""信号不足时用产品档案基线"""
angles = product.get("text_angles") or []
style = product.get("style_tone", "素人分享风")
fragment = ""
if angles:
fragment = f"优先覆盖以下文案角度:{''.join(angles[:3])}。风格调性:{style}"
return {
"recent_preference": f"冷启动(历史信号{injected_count}条,不足{FLYWHEEL_COLD_START}条),使用产品档案基线",
"reject_reasons": [],
"injected_count": injected_count,
"prompt_fragment": fragment,
}
def _build_prompt_fragment(
top_angles: list[str],
reject_reasons: list[str],
product: dict,
) -> str:
"""
组装注入文案 prompt 的片段
越积累越精准1次=全靠基线10次=知道偏好角度30次=措辞从"供参考"升为明确指令
"""
lines: list[str] = []
if top_angles:
lines.append(f"【偏好角度参考】历史选择偏好:{''.join(top_angles)},请优先采用这些角度方向。")
if reject_reasons:
formatted = "".join(f"{r}" for r in reject_reasons)
lines.append(f"【打回原因参考】以下问题请主动规避:{formatted}")
# L1 品牌基线(产品档案 custom_prompt
custom = (product.get("custom_prompt") or "").strip()
if custom:
lines.append(f"【品牌基线】{custom}")
return "\n".join(lines)
def collect_preference_event(
signal_type: str,
user_id: int,
workspace_id: int,
product_id: int,
angle_label: str = "",
reason: str = "",
weights: dict[str, int] | None = None,
) -> dict:
"""
构造 preference_event 行(由业务接口内部调用,不暴露给前端)
返回待插 DB 的字段 dict
"""
from .constants import FLYWHEEL_WEIGHTS
w_map = weights or FLYWHEEL_WEIGHTS
weight = w_map.get(signal_type, 0)
return {
"signal_type": signal_type,
"signal_weight": weight,
"user_id": user_id,
"workspace_id": workspace_id,
"product_id": product_id,
"angle_label": angle_label,
"reason": reason,
"data_ownership": "client_data", # 原始行为信号归客户PRD §3 data_ownership
}