fix: harden online validation flows
This commit is contained in:
@@ -7,7 +7,7 @@ import { getErrorAction } from '@/types/errors';
|
||||
|
||||
export function ApiKeysTab() {
|
||||
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 [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
@@ -16,7 +16,14 @@ export function ApiKeysTab() {
|
||||
const keyInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 当前选中 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(); }, []);
|
||||
|
||||
@@ -30,7 +37,7 @@ export function ApiKeysTab() {
|
||||
|
||||
// 点列表「修改」:把该 provider 填回表单、清空 key 待重录、聚焦输入框(覆盖式改 key)
|
||||
function handleEdit(provider: string) {
|
||||
setForm({ provider, api_key: '' });
|
||||
setForm({ provider: providerOf(provider), api_key: '' });
|
||||
setError('');
|
||||
setSuccess('');
|
||||
keyInputRef.current?.focus();
|
||||
@@ -42,7 +49,7 @@ export function ApiKeysTab() {
|
||||
setSaving(true); setError(''); setSuccess('');
|
||||
try {
|
||||
await api.post('/api/v1/api-keys', form);
|
||||
setForm({ provider: 'openai', api_key: '' });
|
||||
setForm({ provider: 'apiports', api_key: '' });
|
||||
await loadKeys();
|
||||
} catch (err) {
|
||||
const apiErr = err as ApiError;
|
||||
@@ -81,7 +88,7 @@ export function ApiKeysTab() {
|
||||
<select id="provider" value={form.provider}
|
||||
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">
|
||||
<option value="openai">主通道 apiports(生图+生文,必填)</option>
|
||||
<option value="apiports">主通道 apiports(生图+生文,必填)</option>
|
||||
<option value="codeproxy">备用通道 codeproxy(apiports 繁忙时自动顶上,选填)</option>
|
||||
<option value="gemini">Gemini(生图备选,选填)</option>
|
||||
</select>
|
||||
@@ -115,7 +122,7 @@ export function ApiKeysTab() {
|
||||
{keys.map((k) => (
|
||||
<li key={k.id} className="flex items-center justify-between py-3 text-sm">
|
||||
<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>
|
||||
<span className="flex items-center gap-3">
|
||||
|
||||
@@ -20,6 +20,8 @@ export function BenchmarksTab() {
|
||||
const [benchmarks, setBenchmarks] = useState<Benchmark[]>([]);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [analyzingId, setAnalyzingId] = useState<number | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => { loadProducts(); }, []);
|
||||
|
||||
@@ -38,16 +40,35 @@ export function BenchmarksTab() {
|
||||
|
||||
async function loadBenchmarks(productId: number) {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const data = await api.get<PaginatedResponse<Benchmark>>(
|
||||
`/api/v1/products/${productId}/benchmarks`
|
||||
);
|
||||
setBenchmarks(data.items);
|
||||
} catch (err) {
|
||||
const e = err as { message?: string };
|
||||
setError(e.message || '标杆笔记加载失败');
|
||||
} finally {
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -70,6 +91,8 @@ export function BenchmarksTab() {
|
||||
{loading ? (
|
||||
<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">
|
||||
{benchmarks.map((b) => (
|
||||
<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"
|
||||
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>
|
||||
))}
|
||||
@@ -118,6 +150,7 @@ export function BenchmarksTab() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showForm && selectedProductId && (
|
||||
|
||||
@@ -55,7 +55,20 @@ async function request<T>(
|
||||
Object.assign(headers, options.headers as Record<string, string>);
|
||||
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();
|
||||
|
||||
if ('code' in body && body.code === ERROR_CODES.SUCCESS) {
|
||||
|
||||
Reference in New Issue
Block a user