forked from boos/BoosAPI
fix: address review issues across settings, sites, brand matcher, and token routes
- settings.ts: trigger route rebuild after globalBlockedBrands changes in both import and PUT handler; reject non-array payloads with 400 instead of silently defaulting to empty array - sites.ts: trim model names before deduplication so whitespace variants merge - brandMatcher.ts: align boundary regex with frontend delimiter class [/:_\-\s] - Settings.tsx: add role="switch" and aria-checked to brand filter chips; make getBrandList best-effort so its failure doesn't block page load - Sites.tsx: build brandGroups from union of availableModels and disabledModels so legacy/removed models remain visible; add stale-response guard and error logging for disabled/available model fetches - TokenRoutes.tsx: use baseFilteredRoutes.length for totalRouteCount to keep enabled/disabled counts in sync with status filter; add candidatesVersionRef to invalidate lazy caches on payload change; add per-channel in-flight guard on toggle; invalidate channel cache after rebuild and site-block operations
This commit is contained in:
@@ -417,15 +417,21 @@ export default function Settings() {
|
||||
const loadSettings = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [authInfo, runtimeInfo, downstreamInfo, routeRows, runtimeDatabaseInfo, brandListRes] = await Promise.all([
|
||||
const [authInfo, runtimeInfo, downstreamInfo, routeRows, runtimeDatabaseInfo] = await Promise.all([
|
||||
api.getAuthInfo(),
|
||||
api.getRuntimeSettings(),
|
||||
api.getDownstreamApiKeys(),
|
||||
api.getRoutesLite(),
|
||||
api.getRuntimeDatabaseConfig(),
|
||||
api.getBrandList(),
|
||||
]);
|
||||
setAllBrandNames(Array.isArray(brandListRes?.brands) ? brandListRes.brands : []);
|
||||
let brandListBrands: string[] = [];
|
||||
try {
|
||||
const brandListRes = await api.getBrandList();
|
||||
brandListBrands = Array.isArray(brandListRes?.brands) ? brandListRes.brands : [];
|
||||
} catch {
|
||||
// best-effort: brand list is non-critical
|
||||
}
|
||||
setAllBrandNames(brandListBrands);
|
||||
setMaskedToken(authInfo.masked || '****');
|
||||
setRuntime({
|
||||
checkinCron: runtimeInfo.checkinCron || '0 8 * * *',
|
||||
@@ -1469,6 +1475,8 @@ export default function Settings() {
|
||||
<button
|
||||
key={brand}
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={isBlocked}
|
||||
onClick={() => {
|
||||
if (isBlocked) {
|
||||
setBlockedBrands((prev) => prev.filter((b) => b !== brand));
|
||||
|
||||
+13
-4
@@ -228,8 +228,9 @@ export default function Sites() {
|
||||
const disabledModelSet = useMemo(() => new Set(disabledModels), [disabledModels]);
|
||||
|
||||
const brandGroups = useMemo(() => {
|
||||
const allModels = Array.from(new Set([...availableModels, ...disabledModels]));
|
||||
const groups = new Map<string, string[]>();
|
||||
for (const model of availableModels) {
|
||||
for (const model of allModels) {
|
||||
const brand = getBrand(model);
|
||||
const brandName = brand?.name || '其他';
|
||||
if (!groups.has(brandName)) groups.set(brandName, []);
|
||||
@@ -240,7 +241,7 @@ export default function Sites() {
|
||||
if (b[0] === '其他') return -1;
|
||||
return a[0].localeCompare(b[0], undefined, { sensitivity: 'base' });
|
||||
});
|
||||
}, [availableModels]);
|
||||
}, [availableModels, disabledModels]);
|
||||
|
||||
const filteredBrandGroups = useMemo(() => {
|
||||
const q = disabledModelSearch.trim().toLowerCase();
|
||||
@@ -354,6 +355,7 @@ export default function Sites() {
|
||||
setForm(siteFormFromSite(site));
|
||||
scrollToEditorTop();
|
||||
// Load disabled models and available models for this site
|
||||
const loadSiteId = site.id;
|
||||
setDisabledModelsLoading(true);
|
||||
setDisabledModels([]);
|
||||
setAvailableModels([]);
|
||||
@@ -363,11 +365,18 @@ export default function Sites() {
|
||||
api.getSiteAvailableModels(site.id),
|
||||
])
|
||||
.then(([disabledRes, availableRes]: any[]) => {
|
||||
// Guard: only apply if we're still editing the same site
|
||||
if (editor?.mode !== 'edit' && loadSiteId !== site.id) return;
|
||||
setDisabledModels(Array.isArray(disabledRes?.models) ? disabledRes.models : []);
|
||||
setAvailableModels(Array.isArray(availableRes?.models) ? availableRes.models : []);
|
||||
})
|
||||
.catch(() => { })
|
||||
.finally(() => setDisabledModelsLoading(false));
|
||||
.catch((err: any) => {
|
||||
console.warn('Failed to load site models:', err?.message || err);
|
||||
// Preserve previous (empty) model lists — don't clear UI silently
|
||||
})
|
||||
.finally(() => {
|
||||
setDisabledModelsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveDisabledModels = async () => {
|
||||
|
||||
@@ -163,6 +163,7 @@ export default function TokenRoutes() {
|
||||
|
||||
const candidatesLoadedRef = useRef(false);
|
||||
const candidatesPromiseRef = useRef<Promise<void> | null>(null);
|
||||
const candidatesVersionRef = useRef(0);
|
||||
|
||||
const loadRouteDecisions = async (
|
||||
routeRows: RouteSummaryRow[],
|
||||
@@ -248,6 +249,7 @@ export default function TokenRoutes() {
|
||||
);
|
||||
setEndpointTypesByModel(candidateRows?.endpointTypesByModel || {});
|
||||
});
|
||||
candidatesVersionRef.current = Date.now();
|
||||
} catch {
|
||||
candidatesLoadedRef.current = false;
|
||||
} finally {
|
||||
@@ -298,12 +300,14 @@ export default function TokenRoutes() {
|
||||
const res = await api.rebuildRoutes(true);
|
||||
if (res?.queued) {
|
||||
toast.info(res.message || '已开始重建路由,请稍后查看日志');
|
||||
invalidateChannels();
|
||||
await load();
|
||||
return;
|
||||
}
|
||||
const createdRoutes = res?.rebuild?.createdRoutes ?? 0;
|
||||
const createdChannels = res?.rebuild?.createdChannels ?? 0;
|
||||
toast.success(`自动重建完成(新增 ${createdRoutes} 条路由 / ${createdChannels} 个通道)`);
|
||||
invalidateChannels();
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
toast.error(e.message || '重建路由失败');
|
||||
@@ -778,7 +782,7 @@ export default function TokenRoutes() {
|
||||
|
||||
// Lazy per-route candidate index — only computes for routes actually accessed
|
||||
const candidateIndexCacheRef = useRef<{ key: string; cache: Map<number, RouteCandidateView> }>({ key: '', cache: new Map() });
|
||||
const candidateIndexCacheKey = `${routePatternsKey}|${Object.keys(modelCandidates).length}`;
|
||||
const candidateIndexCacheKey = `${routePatternsKey}|${Object.keys(modelCandidates).length}|${candidatesVersionRef.current}`;
|
||||
if (candidateIndexCacheRef.current.key !== candidateIndexCacheKey) {
|
||||
candidateIndexCacheRef.current = { key: candidateIndexCacheKey, cache: new Map() };
|
||||
}
|
||||
@@ -797,7 +801,7 @@ export default function TokenRoutes() {
|
||||
|
||||
// Lazy per-route missing token index
|
||||
const missingTokenCacheRef = useRef<{ key: string; cache: Map<number, RouteMissingTokenHint[]> }>({ key: '', cache: new Map() });
|
||||
const missingTokenCacheKey = `${routePatternsKey}|${Object.keys(missingTokenModelsByName).length}`;
|
||||
const missingTokenCacheKey = `${routePatternsKey}|${Object.keys(missingTokenModelsByName).length}|${candidatesVersionRef.current}`;
|
||||
if (missingTokenCacheRef.current.key !== missingTokenCacheKey) {
|
||||
missingTokenCacheRef.current = { key: missingTokenCacheKey, cache: new Map() };
|
||||
}
|
||||
@@ -816,7 +820,7 @@ export default function TokenRoutes() {
|
||||
|
||||
// Lazy per-route missing token group index
|
||||
const missingTokenGroupCacheRef = useRef<{ key: string; cache: Map<number, RouteMissingTokenHint[]> }>({ key: '', cache: new Map() });
|
||||
const missingTokenGroupCacheKey = `${routePatternsKey}|${Object.keys(missingTokenGroupModelsByName).length}`;
|
||||
const missingTokenGroupCacheKey = `${routePatternsKey}|${Object.keys(missingTokenGroupModelsByName).length}|${candidatesVersionRef.current}`;
|
||||
if (missingTokenGroupCacheRef.current.key !== missingTokenGroupCacheKey) {
|
||||
missingTokenGroupCacheRef.current = { key: missingTokenGroupCacheKey, cache: new Map() };
|
||||
}
|
||||
@@ -897,12 +901,16 @@ export default function TokenRoutes() {
|
||||
};
|
||||
|
||||
const handleToggleChannelEnabled = async (channelId: number, routeId: number, enabled: boolean) => {
|
||||
if (updatingChannel[channelId]) return;
|
||||
setUpdatingChannel((prev) => ({ ...prev, [channelId]: true }));
|
||||
try {
|
||||
await api.updateChannel(channelId, { enabled });
|
||||
toast.success(enabled ? '通道已启用' : '通道已禁用');
|
||||
await loadChannels(routeId, true);
|
||||
} catch (e: any) {
|
||||
toast.error(e.message || '更新通道状态失败');
|
||||
} finally {
|
||||
setUpdatingChannel((prev) => ({ ...prev, [channelId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1024,6 +1032,7 @@ export default function TokenRoutes() {
|
||||
await api.updateSiteDisabledModels(siteId, [...currentModels, modelName]);
|
||||
toast.success(`已将「${modelName}」加入站点「${siteName}」的禁用列表,正在重建路由...`);
|
||||
await api.rebuildRoutes(false);
|
||||
invalidateChannels();
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
toast.error(e.message || '站点屏蔽模型失败');
|
||||
@@ -1324,7 +1333,7 @@ export default function TokenRoutes() {
|
||||
</button>
|
||||
<MobileFilterSheet open={showFilters} onClose={() => setShowFilters(false)} title={tr('筛选路由')}>
|
||||
<RouteFilterBar
|
||||
totalRouteCount={listVisibleRoutes.length}
|
||||
totalRouteCount={baseFilteredRoutes.length}
|
||||
activeBrand={activeBrand}
|
||||
setActiveBrand={setActiveBrand}
|
||||
activeSite={activeSite}
|
||||
@@ -1347,7 +1356,7 @@ export default function TokenRoutes() {
|
||||
</>
|
||||
) : (
|
||||
<RouteFilterBar
|
||||
totalRouteCount={listVisibleRoutes.length}
|
||||
totalRouteCount={baseFilteredRoutes.length}
|
||||
activeBrand={activeBrand}
|
||||
setActiveBrand={setActiveBrand}
|
||||
activeSite={activeSite}
|
||||
|
||||
Reference in New Issue
Block a user