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:
Hureru
2026-03-22 20:16:29 +08:00
parent 911d76fa08
commit 26be7c024a
6 changed files with 69 additions and 20 deletions
+11 -3
View File
@@ -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
View File
@@ -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 () => {
+14 -5
View File
@@ -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}