8维草案(banana不变量+俊达配方+3链接):标题公式/开头钩子/正文结构/
卖点呈现/情绪语气emoji/目标人群/话题标签/视觉配图节奏。
- _benchmark_prompt.py: 8维vision prompt(不打分,提炼可复用配方)
- benchmark_analyzer.py: 引擎核心(vision读截图+手填亮点→8维JSON,失败兜底)
- benchmarks.py: POST /benchmarks/{id}/analyze 端点(写features_json,流转analyze_status)
端到端实测:8维齐全0缺失,真调LLM(conf0.9非兜底),落库done,质量高(识别收藏钩子/成分背书/诚实标未体现)
211 lines
7.4 KiB
Python
211 lines
7.4 KiB
Python
"""
|
||
app/api/v1/benchmarks.py — 标杆笔记 + 违禁词路由(管理员)
|
||
主通道:截图+手填亮点(不做原始抓取)。
|
||
"""
|
||
|
||
import logging
|
||
from typing import Annotated
|
||
|
||
from fastapi import APIRouter, Depends
|
||
from pydantic import BaseModel
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.database import get_db
|
||
from app.core.response import ok, paginate, raise_not_found
|
||
from app.middleware.workspace_guard import CurrentUser, require_admin, require_write_permission
|
||
from app.models.product import BannedWord, BenchmarkNote
|
||
|
||
logger = logging.getLogger(__name__)
|
||
router = APIRouter(tags=["products"])
|
||
|
||
|
||
class BenchmarkCreate(BaseModel):
|
||
screenshot_url: str | None = None
|
||
highlights: str | None = None # 手填亮点(主通道)
|
||
link_url: str | None = None # 可选,不做自动抓取
|
||
|
||
|
||
class BannedWordCreate(BaseModel):
|
||
word: str
|
||
level: str # auto_fix | soft_warn | hard_block
|
||
replacement: str | None = None
|
||
updatable: bool = True
|
||
|
||
|
||
def _fmt_benchmark(b: BenchmarkNote) -> dict:
|
||
import json
|
||
features = None
|
||
if b.features_json:
|
||
try:
|
||
features = json.loads(b.features_json)
|
||
except Exception:
|
||
features = None
|
||
return {
|
||
"id": b.id, "product_id": b.product_id,
|
||
"screenshot_url": b.screenshot_url,
|
||
"highlights": b.highlights, "link_url": b.link_url,
|
||
"features": features, "analyze_status": b.analyze_status,
|
||
"created_at": b.created_at.isoformat(),
|
||
}
|
||
|
||
|
||
def _fmt_banned(bw: BannedWord) -> dict:
|
||
return {
|
||
"id": bw.id, "word": bw.word, "level": bw.level,
|
||
"replacement": bw.replacement, "updatable": bw.updatable,
|
||
"workspace_id": bw.workspace_id,
|
||
}
|
||
|
||
|
||
# ── 标杆笔记 ────────────────────────────────────────────────
|
||
@router.get("/products/{product_id}/benchmarks")
|
||
def list_benchmarks(
|
||
product_id: int,
|
||
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
items = (
|
||
db.query(BenchmarkNote)
|
||
.filter(
|
||
BenchmarkNote.product_id == product_id,
|
||
BenchmarkNote.workspace_id == current_user.workspace_id,
|
||
)
|
||
.all()
|
||
)
|
||
return ok({"items": [_fmt_benchmark(b) for b in items]})
|
||
|
||
|
||
@router.post("/products/{product_id}/benchmarks")
|
||
def create_benchmark(
|
||
product_id: int, body: BenchmarkCreate,
|
||
current_user: Annotated[CurrentUser, Depends(require_admin)] = None,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
b = BenchmarkNote(
|
||
workspace_id=current_user.workspace_id,
|
||
product_id=product_id,
|
||
screenshot_url=body.screenshot_url,
|
||
highlights=body.highlights,
|
||
link_url=body.link_url,
|
||
)
|
||
db.add(b); db.commit(); db.refresh(b)
|
||
return ok(_fmt_benchmark(b))
|
||
|
||
|
||
@router.post("/benchmarks/{benchmark_id}/analyze")
|
||
async def analyze_benchmark_note(
|
||
benchmark_id: int,
|
||
current_user: Annotated[CurrentUser, Depends(require_admin)] = None,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""
|
||
第2环:对已存标杆笔记跑 8 维拆解。
|
||
输入源=screenshot_url 截图(vision) + highlights 手填亮点(参考)。
|
||
结果写入 features_json,analyze_status 流转 analyzing→done/failed。
|
||
key 链路遵基石B:plain_key 只活局部,立即清零。
|
||
"""
|
||
import json
|
||
from app.core.response import raise_business
|
||
from app.models.workspace import UserApiKey
|
||
from app.utils.fernet_utils import decrypt_key
|
||
from app.services.ai_engine.gemini_factory import build_ai_clients
|
||
from app.services.ai_engine.benchmark_analyzer import analyze_benchmark
|
||
|
||
b = db.query(BenchmarkNote).filter(
|
||
BenchmarkNote.id == benchmark_id,
|
||
BenchmarkNote.workspace_id == current_user.workspace_id,
|
||
).first()
|
||
if not b:
|
||
raise_not_found("标杆笔记不存在")
|
||
if not b.screenshot_url and not b.highlights:
|
||
raise_business("该标杆无截图也无手填亮点,无法分析")
|
||
|
||
# 读截图字节(screenshot_url 存的是上传后的绝对/相对路径)
|
||
screenshot = None
|
||
if b.screenshot_url:
|
||
try:
|
||
with open(b.screenshot_url, "rb") as f:
|
||
screenshot = f.read()
|
||
except Exception as e:
|
||
logger.warning("标杆截图读取失败 id=%s: %s", benchmark_id, e)
|
||
|
||
api_key_row = db.query(UserApiKey).filter(
|
||
UserApiKey.user_id == current_user.user_id,
|
||
UserApiKey.workspace_id == current_user.workspace_id,
|
||
UserApiKey.provider.in_(["openai", "apiports"]),
|
||
).first()
|
||
if not api_key_row:
|
||
raise_business("未配置 API Key,请先在设置中录入")
|
||
plain_key = decrypt_key(api_key_row.encrypted_key)
|
||
clients = build_ai_clients(plain_key)
|
||
plain_key = None
|
||
|
||
b.analyze_status = "analyzing"; db.commit()
|
||
try:
|
||
result = await analyze_benchmark(clients, screenshot, b.highlights)
|
||
finally:
|
||
await clients.aclose()
|
||
|
||
b.features_json = json.dumps(result, ensure_ascii=False)
|
||
b.analyze_status = "done" if result.get("source") != "fallback" else "failed"
|
||
db.commit(); db.refresh(b)
|
||
logger.info("benchmark analyzed id=%s status=%s source=%s",
|
||
benchmark_id, b.analyze_status, result.get("source"))
|
||
return ok({**_fmt_benchmark(b), "features": result, "analyze_status": b.analyze_status})
|
||
@router.get("/banned-words")
|
||
def list_banned_words(
|
||
page: int = 1, page_size: int = 50,
|
||
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
q = db.query(BannedWord).filter(BannedWord.workspace_id == current_user.workspace_id)
|
||
total = q.count()
|
||
items = q.offset((page - 1) * page_size).limit(page_size).all()
|
||
return ok(paginate([_fmt_banned(bw) for bw in items], total, page, page_size))
|
||
|
||
|
||
@router.post("/banned-words")
|
||
def create_banned_word(
|
||
body: BannedWordCreate,
|
||
current_user: Annotated[CurrentUser, Depends(require_admin)] = None,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
bw = BannedWord(
|
||
workspace_id=current_user.workspace_id,
|
||
word=body.word, level=body.level,
|
||
replacement=body.replacement, updatable=body.updatable,
|
||
)
|
||
db.add(bw); db.commit(); db.refresh(bw)
|
||
return ok(_fmt_banned(bw))
|
||
|
||
|
||
@router.put("/banned-words/{word_id}")
|
||
def update_banned_word(
|
||
word_id: int, body: BannedWordCreate,
|
||
current_user: Annotated[CurrentUser, Depends(require_admin)] = None,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
bw = db.query(BannedWord).filter(BannedWord.id == word_id, BannedWord.workspace_id == current_user.workspace_id).first()
|
||
if not bw:
|
||
raise_not_found("违禁词不存在")
|
||
if not bw.updatable:
|
||
from app.core.response import raise_forbidden
|
||
raise_forbidden("该违禁词不可修改")
|
||
bw.word = body.word; bw.level = body.level
|
||
bw.replacement = body.replacement; bw.updatable = body.updatable
|
||
db.commit(); db.refresh(bw)
|
||
return ok(_fmt_banned(bw))
|
||
|
||
|
||
@router.delete("/banned-words/{word_id}")
|
||
def delete_banned_word(
|
||
word_id: int,
|
||
current_user: Annotated[CurrentUser, Depends(require_admin)] = None,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
bw = db.query(BannedWord).filter(BannedWord.id == word_id, BannedWord.workspace_id == current_user.workspace_id).first()
|
||
if not bw:
|
||
raise_not_found("违禁词不存在")
|
||
db.delete(bw); db.commit()
|
||
return ok({"deleted": word_id})
|