forked from boos/BoosAPI
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 = {
|
const inputStyle: React.CSSProperties = {
|
||||||
width: '100%', padding: '10px 14px', border: '1px solid var(--color-border)',
|
width: '100%', padding: '10px 14px', border: '1px solid var(--color-border)',
|
||||||
borderRadius: 'var(--radius-sm)', fontSize: 13, outline: 'none',
|
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" /> : '刷新'}
|
{actionLoading[`refresh-${a.id}`] ? <span className="spinner spinner-sm" /> : '刷新'}
|
||||||
</button>
|
</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" /> : '模型'}
|
{actionLoading[`models-${a.id}`] ? <span className="spinner spinner-sm" /> : '模型'}
|
||||||
</button>
|
</button>
|
||||||
{capabilities.canCheckin && (
|
{capabilities.canCheckin && (
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { act, create, type ReactTestInstance } from 'react-test-renderer';
|
import { act, create, type ReactTestInstance } from 'react-test-renderer';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { ToastProvider } from '../components/Toast.js';
|
import { ToastProvider } from '../components/Toast.js';
|
||||||
import Accounts from './Accounts.js';
|
import Accounts from './Accounts.js';
|
||||||
|
|
||||||
const { apiMock } = vi.hoisted(() => ({
|
const { apiMock, toastMock } = vi.hoisted(() => ({
|
||||||
apiMock: {
|
apiMock: {
|
||||||
getAccounts: vi.fn(),
|
getAccounts: vi.fn(),
|
||||||
getSites: vi.fn(),
|
getSites: vi.fn(),
|
||||||
updateAccount: vi.fn(),
|
updateAccount: vi.fn(),
|
||||||
refreshAccountHealth: 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,
|
api: apiMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/Toast.js', () => ({
|
||||||
|
ToastProvider: ({ children }: { children: ReactNode }) => children,
|
||||||
|
useToast: () => toastMock,
|
||||||
|
}));
|
||||||
|
|
||||||
function collectText(node: ReactTestInstance): string {
|
function collectText(node: ReactTestInstance): string {
|
||||||
return (node.children || []).map((child) => {
|
return (node.children || []).map((child) => {
|
||||||
if (typeof child === 'string') return child;
|
if (typeof child === 'string') return child;
|
||||||
@@ -89,4 +102,47 @@ describe('Accounts edit panel', () => {
|
|||||||
root?.unmount();
|
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