feat: refine model refresh toasts and failure handling
This commit is contained in:
@@ -348,6 +348,42 @@ export default function Accounts() {
|
||||
}
|
||||
};
|
||||
|
||||
const formatModelSuccess = (refresh: any) => {
|
||||
const models = Array.isArray(refresh?.modelsPreview) ? refresh.modelsPreview : [];
|
||||
const count = Number.isFinite(refresh?.modelCount) ? refresh.modelCount : models.length;
|
||||
if (models.length === 0) return `已获取到模型(共 ${count} 个)`;
|
||||
const preview = models.slice(0, 6).join('、');
|
||||
const suffix = `(共 ${count} 个)`;
|
||||
return `已获取到模型:${preview}${suffix}`;
|
||||
};
|
||||
|
||||
const formatModelFailure = (refresh: any, messageFallback?: string) => {
|
||||
const code = refresh?.errorCode;
|
||||
if (code === 'timeout') return '模型获取失败(请求超时)';
|
||||
if (code === 'unauthorized') return '模型获取失败,API Key 已无效';
|
||||
if (code === 'empty_models') return '模型获取失败:未获取到可用模型';
|
||||
return messageFallback || refresh?.errorMessage || '模型获取失败';
|
||||
};
|
||||
|
||||
const handleCheckModels = async (accountId: number) => {
|
||||
const key = `models-${accountId}`;
|
||||
setActionLoading(s => ({ ...s, [key]: true }));
|
||||
try {
|
||||
const result = await api.checkModels(accountId);
|
||||
const refresh = result?.refresh;
|
||||
if (!refresh || refresh.status !== 'success') {
|
||||
toast.error(formatModelFailure(refresh, result?.message));
|
||||
} else {
|
||||
toast.success(formatModelSuccess(refresh));
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast.error(e.message || '模型获取失败');
|
||||
} finally {
|
||||
setActionLoading(s => ({ ...s, [key]: false }));
|
||||
void load();
|
||||
}
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%', padding: '10px 14px', border: '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)', fontSize: 13, outline: 'none',
|
||||
@@ -1461,7 +1497,7 @@ export default function Accounts() {
|
||||
{actionLoading[`refresh-${a.id}`] ? <span className="spinner spinner-sm" /> : '刷新'}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => withLoading(`models-${a.id}`, () => api.checkModels(a.id), '模型已更新')} disabled={actionLoading[`models-${a.id}`]} className="btn btn-link btn-link-info">
|
||||
<button onClick={() => handleCheckModels(a.id)} disabled={actionLoading[`models-${a.id}`]} className="btn btn-link btn-link-info">
|
||||
{actionLoading[`models-${a.id}`] ? <span className="spinner spinner-sm" /> : '模型'}
|
||||
</button>
|
||||
{capabilities.canCheckin && (
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, create, type ReactTestInstance } from 'react-test-renderer';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ToastProvider } from '../components/Toast.js';
|
||||
import Accounts from './Accounts.js';
|
||||
|
||||
const { apiMock } = vi.hoisted(() => ({
|
||||
const { apiMock, toastMock } = vi.hoisted(() => ({
|
||||
apiMock: {
|
||||
getAccounts: vi.fn(),
|
||||
getSites: vi.fn(),
|
||||
updateAccount: vi.fn(),
|
||||
refreshAccountHealth: vi.fn(),
|
||||
checkModels: vi.fn(),
|
||||
},
|
||||
toastMock: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
toast: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -17,6 +25,11 @@ vi.mock('../api.js', () => ({
|
||||
api: apiMock,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Toast.js', () => ({
|
||||
ToastProvider: ({ children }: { children: ReactNode }) => children,
|
||||
useToast: () => toastMock,
|
||||
}));
|
||||
|
||||
function collectText(node: ReactTestInstance): string {
|
||||
return (node.children || []).map((child) => {
|
||||
if (typeof child === 'string') return child;
|
||||
@@ -89,4 +102,47 @@ describe('Accounts edit panel', () => {
|
||||
root?.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows model refresh toast on success', async () => {
|
||||
apiMock.checkModels.mockResolvedValue({
|
||||
refresh: {
|
||||
status: 'success',
|
||||
modelCount: 2,
|
||||
modelsPreview: ['gpt-4', 'gpt-3.5'],
|
||||
},
|
||||
});
|
||||
|
||||
let root: ReturnType<typeof create> | null = null;
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<MemoryRouter initialEntries={['/accounts']}>
|
||||
<ToastProvider>
|
||||
<Accounts />
|
||||
</ToastProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const modelButtons = root.root.findAll((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.onClick === 'function'
|
||||
&& typeof node.props.className === 'string'
|
||||
&& node.props.className.includes('btn-link-info')
|
||||
&& collectText(node).trim() === '模型'
|
||||
));
|
||||
expect(modelButtons.length).toBeGreaterThan(0);
|
||||
|
||||
await act(async () => {
|
||||
await modelButtons[0]!.props.onClick();
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(toastMock.success).toHaveBeenCalledWith(expect.stringContaining('已获取到模型'));
|
||||
expect(toastMock.success).toHaveBeenCalledWith(expect.stringContaining('(共 2 个)'));
|
||||
} finally {
|
||||
root?.unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user