fix: harden online validation flows

This commit is contained in:
yangqianqian
2026-07-01 10:56:08 +08:00
parent da137f099f
commit 033ef89536
7 changed files with 142 additions and 15 deletions

View File

@@ -8,6 +8,7 @@ url 字段不接收token站固定自家站架构方案§二
import logging
from typing import Annotated
import httpx
from fastapi import APIRouter, Depends
from pydantic import BaseModel, field_validator
from sqlalchemy.orm import Session
@@ -32,7 +33,8 @@ class CreateApiKeyRequest(BaseModel):
def provider_not_empty(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("provider 不能为空")
return v.strip().lower()
provider = v.strip().lower()
return "apiports" if provider == "openai" else provider
@field_validator("api_key")
@classmethod
@@ -49,14 +51,47 @@ class TestApiKeyRequest(CreateApiKeyRequest):
def _format_key(k: UserApiKey) -> dict:
"""只显 provider + key_last4不暴露余额/用量/encrypted_key红线"""
provider = "apiports" if k.provider == "openai" else k.provider
return {
"id": k.id,
"provider": k.provider,
"provider": provider,
"key_last4": k.key_last4,
"created_at": k.created_at.isoformat(),
}
def _provider_aliases(provider: str) -> list[str]:
if provider == "apiports":
return ["apiports", "openai"]
return [provider]
def _upstream_error_message(provider: str, exc: Exception) -> str:
if isinstance(exc, httpx.HTTPStatusError):
status = exc.response.status_code
try:
detail = exc.response.json()
except Exception:
detail = (exc.response.text or "")[:160]
if status in (401, 403):
hint = "Key 无效、权限不足或模型未开通"
elif status == 402:
hint = "账户余额不足或未开通计费"
elif status == 429:
hint = "请求过频或额度受限"
elif status >= 500:
hint = "中转站服务暂时不可用"
else:
hint = "中转站返回异常"
logger.warning("API key test failed provider=%s status=%s detail=%s", provider, status, detail)
return f"Key 测试失败:{provider} 返回 {status}{hint}"
if isinstance(exc, httpx.ReadTimeout):
return f"Key 测试失败:{provider} 在测试超时时间内无响应,请稍后重试或先保存后跑任务验证"
if isinstance(exc, httpx.ConnectError):
return f"Key 测试失败:无法连接 {provider} 中转站,请检查服务器网络或 Base URL"
return f"Key 测试失败:{type(exc).__name__}"
# ── 路由 ───────────────────────────────────────────────────
@router.get("")
def list_api_keys(
@@ -94,11 +129,12 @@ def create_api_key(
.filter(
UserApiKey.user_id == current_user.user_id,
UserApiKey.workspace_id == current_user.workspace_id,
UserApiKey.provider == body.provider,
UserApiKey.provider.in_(_provider_aliases(body.provider)),
)
.first()
)
if existing:
existing.provider = body.provider
existing.encrypted_key = encrypted
existing.key_last4 = last4
db.commit()
@@ -126,13 +162,12 @@ async def test_api_key(
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
):
"""用客户输入的明文 key 做一次轻量连通性测试,不落库。"""
import httpx
from app.core.config import get_settings
from app.core.response import raise_business
settings = get_settings()
provider = body.provider
if provider in ("openai", "apiports"):
if provider == "apiports":
base = (settings.APIPORTS_BASE_URL or settings.IMAGE_API_BASE).rstrip("/")
model = settings.MODEL_TEXT
elif provider == "codeproxy":
@@ -144,7 +179,7 @@ async def test_api_key(
raise_business("服务端未配置该 Provider 的 Base URL")
try:
async with httpx.AsyncClient(timeout=12) as client:
async with httpx.AsyncClient(timeout=45) as client:
resp = await client.post(
f"{base}/chat/completions",
headers={"Authorization": f"Bearer {body.api_key}"},
@@ -156,7 +191,7 @@ async def test_api_key(
)
resp.raise_for_status()
except Exception as exc:
raise_business(f"Key 测试失败:{type(exc).__name__}")
raise_business(_upstream_error_message(provider, exc))
logger.info("API key test ok: user=%s provider=%s", current_user.user_id, provider)
return ok({"ok": True, "provider": provider})

View File

@@ -165,6 +165,20 @@ def build_fission_prompt(
kw_parts = [x for x in ([name] + sp_words + ([audience] if audience != "未提供" else [])) if x and x != "未提供"]
kw_line = "".join(kw_parts) if kw_parts else ""
benchmark_refs = prod.get("benchmark_refs") or []
benchmark_lines: list[str] = []
for idx, ref in enumerate(benchmark_refs[:3], start=1):
if not isinstance(ref, dict):
continue
compact = []
for key in ("title_style", "cover_hook", "pain_point", "scene", "selling_angle", "visual_structure"):
val = ref.get(key)
if val:
compact.append(f"{key}:{val}")
if compact:
benchmark_lines.append(f"标杆配方{idx}" + "".join(compact))
benchmark_block = "\n".join(benchmark_lines) if benchmark_lines else "未选择或未分析完成"
# D3修复按套序循环分配 A/B/C 叙事主线
narrative_keys = list(TEXT_NARRATIVE_BY_STRATEGY.keys()) # ["A","B","C"]
narrative_lines = []
@@ -182,6 +196,7 @@ def build_fission_prompt(
产品卖点:{''.join(points) or '未提供'}
目标人群:{audience}
关键词:{kw_line}
标杆8维配方参考{benchmark_block}
裂变维度:{''.join(dims)}
爆款参考度:{strategy['level_label']}{strategy['prompt_rule']}
生成数量:{note_count}

View File

@@ -130,7 +130,7 @@ def execute_fission_pipeline(db: Session, clients, fission_id: int, source_task_
from app.models.fission import FissionTask, FissionNote
from app.models.task import GenerationTask
from app.models.product import Product
from app.workers.pipeline_steps import build_product_dict
from app.workers.pipeline_steps import build_product_dict, load_benchmark_features
from app.services.ai_engine.fission_prompt import build_fission_prompt
ft = db.query(FissionTask).filter(FissionTask.id == fission_id).first()
@@ -150,6 +150,7 @@ def execute_fission_pipeline(db: Session, clients, fission_id: int, source_task_
return {"fission_id": fission_id, "status": "failed", "reason": "源任务或产品缺失"}
product = build_product_dict(product_row)
product["benchmark_refs"] = load_benchmark_features(db, src, ft.workspace_id)
image_count = max(1, src.image_count or 3)
banned = [] # 违禁词分级表后续接入;评分器内置 BANNED_WORDS_DEFAULT 已兜底

View File

@@ -87,6 +87,29 @@ def create_generation_task(
# 第2环关联标杆笔记ID存库JSON list。pipeline 据此读 features_json 注入文案 prompt。
import json
_bids = getattr(body, "benchmark_ids", None) or []
if _bids:
from app.models.product import BenchmarkNote
ids = [int(i) for i in _bids]
rows = (
db.query(BenchmarkNote)
.filter(
BenchmarkNote.id.in_(ids),
BenchmarkNote.workspace_id == current_user.workspace_id,
BenchmarkNote.product_id == body.product_id,
)
.all()
)
by_id = {int(b.id): b for b in rows}
missing = [i for i in ids if i not in by_id]
if missing:
raise_business(f"标杆笔记不存在或不属于当前产品:{missing[:3]}")
not_ready = [
i for i in ids
if by_id[i].analyze_status != "done" or by_id[i].analysis_source == "fallback"
]
if not_ready:
raise_business("所选标杆尚未分析完成,请先到系统管理里点击「分析标杆」")
benchmark_ids_json = json.dumps([int(i) for i in _bids]) if _bids else None
task = GenerationTask(