fix: harden online validation flows
This commit is contained in:
@@ -8,6 +8,7 @@ url 字段不接收,token站固定自家站(架构方案§二)。
|
|||||||
import logging
|
import logging
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
|
import httpx
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from pydantic import BaseModel, field_validator
|
from pydantic import BaseModel, field_validator
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -32,7 +33,8 @@ class CreateApiKeyRequest(BaseModel):
|
|||||||
def provider_not_empty(cls, v: str) -> str:
|
def provider_not_empty(cls, v: str) -> str:
|
||||||
if not v or not v.strip():
|
if not v or not v.strip():
|
||||||
raise ValueError("provider 不能为空")
|
raise ValueError("provider 不能为空")
|
||||||
return v.strip().lower()
|
provider = v.strip().lower()
|
||||||
|
return "apiports" if provider == "openai" else provider
|
||||||
|
|
||||||
@field_validator("api_key")
|
@field_validator("api_key")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -49,14 +51,47 @@ class TestApiKeyRequest(CreateApiKeyRequest):
|
|||||||
|
|
||||||
def _format_key(k: UserApiKey) -> dict:
|
def _format_key(k: UserApiKey) -> dict:
|
||||||
"""只显 provider + key_last4,不暴露余额/用量/encrypted_key(红线)。"""
|
"""只显 provider + key_last4,不暴露余额/用量/encrypted_key(红线)。"""
|
||||||
|
provider = "apiports" if k.provider == "openai" else k.provider
|
||||||
return {
|
return {
|
||||||
"id": k.id,
|
"id": k.id,
|
||||||
"provider": k.provider,
|
"provider": provider,
|
||||||
"key_last4": k.key_last4,
|
"key_last4": k.key_last4,
|
||||||
"created_at": k.created_at.isoformat(),
|
"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("")
|
@router.get("")
|
||||||
def list_api_keys(
|
def list_api_keys(
|
||||||
@@ -94,11 +129,12 @@ def create_api_key(
|
|||||||
.filter(
|
.filter(
|
||||||
UserApiKey.user_id == current_user.user_id,
|
UserApiKey.user_id == current_user.user_id,
|
||||||
UserApiKey.workspace_id == current_user.workspace_id,
|
UserApiKey.workspace_id == current_user.workspace_id,
|
||||||
UserApiKey.provider == body.provider,
|
UserApiKey.provider.in_(_provider_aliases(body.provider)),
|
||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if existing:
|
if existing:
|
||||||
|
existing.provider = body.provider
|
||||||
existing.encrypted_key = encrypted
|
existing.encrypted_key = encrypted
|
||||||
existing.key_last4 = last4
|
existing.key_last4 = last4
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -126,13 +162,12 @@ async def test_api_key(
|
|||||||
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
|
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
|
||||||
):
|
):
|
||||||
"""用客户输入的明文 key 做一次轻量连通性测试,不落库。"""
|
"""用客户输入的明文 key 做一次轻量连通性测试,不落库。"""
|
||||||
import httpx
|
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
from app.core.response import raise_business
|
from app.core.response import raise_business
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
provider = body.provider
|
provider = body.provider
|
||||||
if provider in ("openai", "apiports"):
|
if provider == "apiports":
|
||||||
base = (settings.APIPORTS_BASE_URL or settings.IMAGE_API_BASE).rstrip("/")
|
base = (settings.APIPORTS_BASE_URL or settings.IMAGE_API_BASE).rstrip("/")
|
||||||
model = settings.MODEL_TEXT
|
model = settings.MODEL_TEXT
|
||||||
elif provider == "codeproxy":
|
elif provider == "codeproxy":
|
||||||
@@ -144,7 +179,7 @@ async def test_api_key(
|
|||||||
raise_business("服务端未配置该 Provider 的 Base URL")
|
raise_business("服务端未配置该 Provider 的 Base URL")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=12) as client:
|
async with httpx.AsyncClient(timeout=45) as client:
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
f"{base}/chat/completions",
|
f"{base}/chat/completions",
|
||||||
headers={"Authorization": f"Bearer {body.api_key}"},
|
headers={"Authorization": f"Bearer {body.api_key}"},
|
||||||
@@ -156,7 +191,7 @@ async def test_api_key(
|
|||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
except Exception as exc:
|
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)
|
logger.info("API key test ok: user=%s provider=%s", current_user.user_id, provider)
|
||||||
return ok({"ok": True, "provider": provider})
|
return ok({"ok": True, "provider": provider})
|
||||||
|
|||||||
@@ -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_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 ""
|
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 叙事主线
|
# D3修复:按套序循环分配 A/B/C 叙事主线
|
||||||
narrative_keys = list(TEXT_NARRATIVE_BY_STRATEGY.keys()) # ["A","B","C"]
|
narrative_keys = list(TEXT_NARRATIVE_BY_STRATEGY.keys()) # ["A","B","C"]
|
||||||
narrative_lines = []
|
narrative_lines = []
|
||||||
@@ -182,6 +196,7 @@ def build_fission_prompt(
|
|||||||
产品卖点:{'、'.join(points) or '未提供'}
|
产品卖点:{'、'.join(points) or '未提供'}
|
||||||
目标人群:{audience}
|
目标人群:{audience}
|
||||||
关键词:{kw_line}
|
关键词:{kw_line}
|
||||||
|
标杆8维配方参考:{benchmark_block}
|
||||||
裂变维度:{'、'.join(dims)}
|
裂变维度:{'、'.join(dims)}
|
||||||
爆款参考度:{strategy['level_label']}。{strategy['prompt_rule']}
|
爆款参考度:{strategy['level_label']}。{strategy['prompt_rule']}
|
||||||
生成数量:{note_count}套
|
生成数量:{note_count}套
|
||||||
|
|||||||
@@ -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.fission import FissionTask, FissionNote
|
||||||
from app.models.task import GenerationTask
|
from app.models.task import GenerationTask
|
||||||
from app.models.product import Product
|
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
|
from app.services.ai_engine.fission_prompt import build_fission_prompt
|
||||||
|
|
||||||
ft = db.query(FissionTask).filter(FissionTask.id == fission_id).first()
|
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": "源任务或产品缺失"}
|
return {"fission_id": fission_id, "status": "failed", "reason": "源任务或产品缺失"}
|
||||||
|
|
||||||
product = build_product_dict(product_row)
|
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)
|
image_count = max(1, src.image_count or 3)
|
||||||
banned = [] # 违禁词分级表后续接入;评分器内置 BANNED_WORDS_DEFAULT 已兜底
|
banned = [] # 违禁词分级表后续接入;评分器内置 BANNED_WORDS_DEFAULT 已兜底
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,29 @@ def create_generation_task(
|
|||||||
# 第2环:关联标杆笔记ID存库(JSON list)。pipeline 据此读 features_json 注入文案 prompt。
|
# 第2环:关联标杆笔记ID存库(JSON list)。pipeline 据此读 features_json 注入文案 prompt。
|
||||||
import json
|
import json
|
||||||
_bids = getattr(body, "benchmark_ids", None) or []
|
_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
|
benchmark_ids_json = json.dumps([int(i) for i in _bids]) if _bids else None
|
||||||
|
|
||||||
task = GenerationTask(
|
task = GenerationTask(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getErrorAction } from '@/types/errors';
|
|||||||
|
|
||||||
export function ApiKeysTab() {
|
export function ApiKeysTab() {
|
||||||
const [keys, setKeys] = useState<ApiKey[]>([]);
|
const [keys, setKeys] = useState<ApiKey[]>([]);
|
||||||
const [form, setForm] = useState<CreateApiKeyRequest>({ provider: 'openai', api_key: '' });
|
const [form, setForm] = useState<CreateApiKeyRequest>({ provider: 'apiports', api_key: '' });
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
@@ -16,7 +16,14 @@ export function ApiKeysTab() {
|
|||||||
const keyInputRef = useRef<HTMLInputElement>(null);
|
const keyInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// 当前选中 provider 是否已录过 → 决定是「保存」还是「更新覆盖」
|
// 当前选中 provider 是否已录过 → 决定是「保存」还是「更新覆盖」
|
||||||
const isOverwrite = keys.some((k) => k.provider === form.provider);
|
const providerOf = (provider: string) => provider === 'openai' ? 'apiports' : provider;
|
||||||
|
const providerLabel = (provider: string) => {
|
||||||
|
const normalized = providerOf(provider);
|
||||||
|
if (normalized === 'apiports') return '主通道 apiports';
|
||||||
|
if (normalized === 'codeproxy') return '备用通道 codeproxy';
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
const isOverwrite = keys.some((k) => providerOf(k.provider) === form.provider);
|
||||||
|
|
||||||
useEffect(() => { loadKeys(); }, []);
|
useEffect(() => { loadKeys(); }, []);
|
||||||
|
|
||||||
@@ -30,7 +37,7 @@ export function ApiKeysTab() {
|
|||||||
|
|
||||||
// 点列表「修改」:把该 provider 填回表单、清空 key 待重录、聚焦输入框(覆盖式改 key)
|
// 点列表「修改」:把该 provider 填回表单、清空 key 待重录、聚焦输入框(覆盖式改 key)
|
||||||
function handleEdit(provider: string) {
|
function handleEdit(provider: string) {
|
||||||
setForm({ provider, api_key: '' });
|
setForm({ provider: providerOf(provider), api_key: '' });
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
keyInputRef.current?.focus();
|
keyInputRef.current?.focus();
|
||||||
@@ -42,7 +49,7 @@ export function ApiKeysTab() {
|
|||||||
setSaving(true); setError(''); setSuccess('');
|
setSaving(true); setError(''); setSuccess('');
|
||||||
try {
|
try {
|
||||||
await api.post('/api/v1/api-keys', form);
|
await api.post('/api/v1/api-keys', form);
|
||||||
setForm({ provider: 'openai', api_key: '' });
|
setForm({ provider: 'apiports', api_key: '' });
|
||||||
await loadKeys();
|
await loadKeys();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const apiErr = err as ApiError;
|
const apiErr = err as ApiError;
|
||||||
@@ -81,7 +88,7 @@ export function ApiKeysTab() {
|
|||||||
<select id="provider" value={form.provider}
|
<select id="provider" value={form.provider}
|
||||||
onChange={(e) => { setError(''); setSuccess(''); setForm((f) => ({ ...f, provider: e.target.value })); }}
|
onChange={(e) => { setError(''); setSuccess(''); setForm((f) => ({ ...f, provider: e.target.value })); }}
|
||||||
className="border border-border-default rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand-orange">
|
className="border border-border-default rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand-orange">
|
||||||
<option value="openai">主通道 apiports(生图+生文,必填)</option>
|
<option value="apiports">主通道 apiports(生图+生文,必填)</option>
|
||||||
<option value="codeproxy">备用通道 codeproxy(apiports 繁忙时自动顶上,选填)</option>
|
<option value="codeproxy">备用通道 codeproxy(apiports 繁忙时自动顶上,选填)</option>
|
||||||
<option value="gemini">Gemini(生图备选,选填)</option>
|
<option value="gemini">Gemini(生图备选,选填)</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -115,7 +122,7 @@ export function ApiKeysTab() {
|
|||||||
{keys.map((k) => (
|
{keys.map((k) => (
|
||||||
<li key={k.id} className="flex items-center justify-between py-3 text-sm">
|
<li key={k.id} className="flex items-center justify-between py-3 text-sm">
|
||||||
<span>
|
<span>
|
||||||
<span className="font-medium">{k.provider}</span>
|
<span className="font-medium">{providerLabel(k.provider)}</span>
|
||||||
<span className="text-text-secondary ml-2">…{k.key_last4}</span>
|
<span className="text-text-secondary ml-2">…{k.key_last4}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-3">
|
<span className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export function BenchmarksTab() {
|
|||||||
const [benchmarks, setBenchmarks] = useState<Benchmark[]>([]);
|
const [benchmarks, setBenchmarks] = useState<Benchmark[]>([]);
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [analyzingId, setAnalyzingId] = useState<number | null>(null);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => { loadProducts(); }, []);
|
useEffect(() => { loadProducts(); }, []);
|
||||||
|
|
||||||
@@ -38,16 +40,35 @@ export function BenchmarksTab() {
|
|||||||
|
|
||||||
async function loadBenchmarks(productId: number) {
|
async function loadBenchmarks(productId: number) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
try {
|
try {
|
||||||
const data = await api.get<PaginatedResponse<Benchmark>>(
|
const data = await api.get<PaginatedResponse<Benchmark>>(
|
||||||
`/api/v1/products/${productId}/benchmarks`
|
`/api/v1/products/${productId}/benchmarks`
|
||||||
);
|
);
|
||||||
setBenchmarks(data.items);
|
setBenchmarks(data.items);
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as { message?: string };
|
||||||
|
setError(e.message || '标杆笔记加载失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function analyzeBenchmark(id: number) {
|
||||||
|
if (!selectedProductId) return;
|
||||||
|
setAnalyzingId(id);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
await api.post(`/api/v1/benchmarks/${id}/analyze`, {});
|
||||||
|
await loadBenchmarks(selectedProductId);
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as { message?: string };
|
||||||
|
setError(e.message || '标杆分析失败,请检查 API Key 后重试');
|
||||||
|
} finally {
|
||||||
|
setAnalyzingId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -70,6 +91,8 @@ export function BenchmarksTab() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-text-secondary text-sm" aria-busy="true">加载中…</div>
|
<div className="text-text-secondary text-sm" aria-busy="true">加载中…</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
{error && <div role="alert" className="rounded bg-red-50 px-3 py-2 text-sm text-status-error">{error}</div>}
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
{benchmarks.map((b) => (
|
{benchmarks.map((b) => (
|
||||||
<div key={b.id} className="border border-border-default rounded-xl overflow-hidden bg-surface-primary">
|
<div key={b.id} className="border border-border-default rounded-xl overflow-hidden bg-surface-primary">
|
||||||
@@ -109,6 +132,15 @@ export function BenchmarksTab() {
|
|||||||
<a href={b.link_url} target="_blank" rel="noopener noreferrer"
|
<a href={b.link_url} target="_blank" rel="noopener noreferrer"
|
||||||
className="text-xs text-brand-orange hover:underline mt-1 block">查看原帖</a>
|
className="text-xs text-brand-orange hover:underline mt-1 block">查看原帖</a>
|
||||||
)}
|
)}
|
||||||
|
<button type="button" onClick={() => analyzeBenchmark(b.id)}
|
||||||
|
disabled={analyzingId === b.id || b.analyze_status === 'analyzing'}
|
||||||
|
className="mt-3 rounded-lg border border-border-default px-3 py-1.5 text-xs text-text-secondary hover:border-brand-orange hover:text-brand-orange disabled:opacity-50">
|
||||||
|
{analyzingId === b.id || b.analyze_status === 'analyzing'
|
||||||
|
? '分析中…'
|
||||||
|
: b.analyze_status === 'done'
|
||||||
|
? '重新分析'
|
||||||
|
: '分析标杆'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -118,6 +150,7 @@ export function BenchmarksTab() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showForm && selectedProductId && (
|
{showForm && selectedProductId && (
|
||||||
|
|||||||
@@ -55,7 +55,20 @@ async function request<T>(
|
|||||||
Object.assign(headers, options.headers as Record<string, string>);
|
Object.assign(headers, options.headers as Record<string, string>);
|
||||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
|
||||||
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers });
|
const url = `${BASE_URL}${path}`;
|
||||||
|
const res = await fetch(url, { ...options, headers });
|
||||||
|
const contentType = res.headers.get('content-type') || '';
|
||||||
|
if (!contentType.includes('application/json')) {
|
||||||
|
const text = await res.text();
|
||||||
|
const apiError = new Error(
|
||||||
|
`接口返回非 JSON(${res.status}),请刷新后重试;如果仍出现,请联系管理员检查后端代理。`
|
||||||
|
) as Error & { code?: number; status?: number; responseText?: string; url?: string };
|
||||||
|
apiError.code = res.status || 50001;
|
||||||
|
apiError.status = res.status;
|
||||||
|
apiError.responseText = text.slice(0, 160);
|
||||||
|
apiError.url = url;
|
||||||
|
throw apiError;
|
||||||
|
}
|
||||||
const body: ApiResponse<T> | ApiError = await res.json();
|
const body: ApiResponse<T> | ApiError = await res.json();
|
||||||
|
|
||||||
if ('code' in body && body.code === ERROR_CODES.SUCCESS) {
|
if ('code' in body && body.code === ERROR_CODES.SUCCESS) {
|
||||||
|
|||||||
Reference in New Issue
Block a user