Files
beige/backend/app/services/flywheel_service.py
yangqianqian 4bed7425a8 A8多套打包+M4归档+R5多图:存量功能备份
A8 多套交付包(packaging_task.py):
- 修复交付包只打第1条混乱note的bug,按ImageCandidate.strategy分A/B/C组
- 每组生独立note_0N夹(6图+文案.txt),同seq留最新去重,老数据兼容
- task74端到端验:3套各6图,独立agent7项交叉验证全过

M4 归档(tasks.py/exports.py/前端):
- list_tasks加date_from/date_to/product_id筛选+product_name批量填(防N+1)
- 新增exports.py:产品JSON导出+标杆CSV导出(UTF-8 BOM)
- 前端HistoryFilters日期/产品筛选+产品列+打回原因红banner
- response.py加raise_param_error;独立agent验A1/A2/A9通过

R5 产品多图(product_images.py/020迁移/前端):
- product_images表+5端点(上传/列/改场景/设主图/删图)
- 生图按ROLE_SCENE_PREFERENCE选对应场景图,回落primary
- 前端ProductImageManager多图画廊

R6 账号config拆页(settings/):
- 配置页按角色拆/settings(运营+组长+admin)+/config(仅admin)
- Key只显末4位不显余额(守红线)

核销表对齐真实代码状态:D1改稿框/M7裂变/E12评图分纠偏为已完成(曾漏回写)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:32:49 +08:00

109 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
app/services/flywheel_service.py — 飞轮信号写入 + 偏好上下文聚合
preference_collector三信号入口选文案/选图/审核)写入 preference_events。
preference_aggregator查最近50条 → 最常选角度 + 打回原因近3条原文拼 prompt。
飞轮不暴露独立埋点端点,只由业务接口内部调用(契约红线)。
"""
import logging
from typing import Any
from sqlalchemy import desc
from sqlalchemy.orm import Session
from app.constants.enums import SIGNAL_WEIGHTS, DataOwnership
from app.middleware.workspace_guard import CurrentUser
from app.models.flywheel import PreferenceEvent
from app.models.task import GenerationTask
logger = logging.getLogger(__name__)
# 实时聚合窗口最近50条事件
_AGGREGATION_WINDOW = 50
def record_signal(
db: Session,
current_user: CurrentUser,
task: GenerationTask,
signal_type: str,
candidate_id: int | None = None,
angle_label: str | None = None,
reason: str | None = None,
signal_meta: str | None = None,
) -> None:
"""
写入飞轮信号。
workspace_id + product_id 都必须有基石C + 按产品分开学)。
signal_weight 用枚举默认值,北哥可校准。
data_ownership 默认 client_data选择行为归客户
signal_metaJSON 扩展(如选图存 strategy不参与角度聚合主逻辑。
"""
weight = SIGNAL_WEIGHTS.get(signal_type, 0)
event = PreferenceEvent(
workspace_id=current_user.workspace_id,
product_id=task.product_id,
task_id=task.id,
user_id=current_user.user_id,
signal_type=signal_type,
signal_weight=weight,
candidate_id=candidate_id,
angle_label=angle_label,
reason=reason,
signal_meta=signal_meta,
data_ownership=DataOwnership.CLIENT_DATA,
)
try:
db.add(event)
db.commit()
logger.info(
"Flywheel signal: type=%s weight=%s user=%s product=%s",
signal_type, weight, current_user.user_id, task.product_id,
)
except Exception:
db.rollback()
logger.error(
"Failed to write preference_event: type=%s user=%s",
signal_type, current_user.user_id,
)
raise
def get_preference_context(
db: Session, workspace_id: int, product_id: int,
product_dict: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
实时聚合偏好上下文最近50条 events供前端展示。
R7断点3统一口径——委托给生产链同一个 aggregate_preference_context权重口径
消除"前端展示按次数/生成按权重"的口径分裂(说一套做一套)。
按 workspace_id + product_id 严格过滤不串数据基石C
"""
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(desc(PreferenceEvent.created_at))
.limit(_AGGREGATION_WINDOW)
.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 or {}, workspace_id, product_id
)
# 前端展示不需要 prompt_fragment注入用剥掉只回摘要+原因+计数
return {
"recent_preference": ctx.get("recent_preference", ""),
"reject_reasons": ctx.get("reject_reasons", []),
"injected_count": ctx.get("injected_count", 0),
}