fix: address review issues in TokenRoutes performance optimization

- Escape modelName/siteName in innerHTML to prevent XSS
- Use content-based routePatternsKey for cache invalidation instead of
  length-only keys that miss content changes
- Extract shared baseFilteredRoutes to eliminate duplicated filter logic
  between enabledCounts and filteredRoutes
- Add candidatesPromiseRef to deduplicate concurrent loadCandidates calls
This commit is contained in:
Hureru
2026-03-22 18:04:46 +08:00
parent 92fd5e034d
commit 3195282c4e
+57 -85
View File
@@ -107,6 +107,10 @@ function getRouteRoutingStrategySuccessMessage(value: RouteRoutingStrategy): str
return '已切换为权重随机策略'; return '已切换为权重随机策略';
} }
function escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
export default function TokenRoutes() { export default function TokenRoutes() {
const navigate = useNavigate(); const navigate = useNavigate();
const [routeSummaries, setRouteSummaries] = useState<RouteSummaryRow[]>([]); const [routeSummaries, setRouteSummaries] = useState<RouteSummaryRow[]>([]);
@@ -158,6 +162,7 @@ export default function TokenRoutes() {
const toast = useToast(); const toast = useToast();
const candidatesLoadedRef = useRef(false); const candidatesLoadedRef = useRef(false);
const candidatesPromiseRef = useRef<Promise<void> | null>(null);
const loadRouteDecisions = async ( const loadRouteDecisions = async (
routeRows: RouteSummaryRow[], routeRows: RouteSummaryRow[],
@@ -226,24 +231,32 @@ export default function TokenRoutes() {
} }
}; };
const loadCandidates = async (force?: boolean) => { const loadCandidates = (force?: boolean) => {
if (candidatesLoadedRef.current && !force) return; if (candidatesLoadedRef.current && !force) return;
if (candidatesPromiseRef.current && !force) return;
candidatesLoadedRef.current = true; candidatesLoadedRef.current = true;
try { const promise = (async () => {
const candidateRows = await api.getModelTokenCandidates(); try {
startTransition(() => { const candidateRows = await api.getModelTokenCandidates();
setModelCandidates((candidateRows?.models || {}) as RouteModelCandidatesByModelName); startTransition(() => {
setMissingTokenModelsByName( setModelCandidates((candidateRows?.models || {}) as RouteModelCandidatesByModelName);
normalizeMissingTokenModels((candidateRows?.modelsWithoutToken || {}) as MissingTokenModelsByName), setMissingTokenModelsByName(
); normalizeMissingTokenModels((candidateRows?.modelsWithoutToken || {}) as MissingTokenModelsByName),
setMissingTokenGroupModelsByName( );
normalizeMissingTokenModels((candidateRows?.modelsMissingTokenGroups || {}) as MissingTokenModelsByName), setMissingTokenGroupModelsByName(
); normalizeMissingTokenModels((candidateRows?.modelsMissingTokenGroups || {}) as MissingTokenModelsByName),
setEndpointTypesByModel(candidateRows?.endpointTypesByModel || {}); );
}); setEndpointTypesByModel(candidateRows?.endpointTypesByModel || {});
} catch { });
candidatesLoadedRef.current = false; } catch {
} candidatesLoadedRef.current = false;
} finally {
if (candidatesPromiseRef.current === promise) {
candidatesPromiseRef.current = null;
}
}
})();
candidatesPromiseRef.current = promise;
}; };
const load = async () => { const load = async () => {
@@ -494,10 +507,11 @@ export default function TokenRoutes() {
}; };
// Stable derived value: only changes when route patterns change (not on enabled toggle) // Stable derived value: only changes when route patterns change (not on enabled toggle)
const routePatternsKey = visibleRouteRows.map((r) => `${r.id}:${r.modelPattern}:${r.routeMode || 'pattern'}`).join(',');
const routePatterns = useMemo( const routePatterns = useMemo(
() => visibleRouteRows.map((r) => ({ id: r.id, modelPattern: r.modelPattern, routeMode: r.routeMode })), () => visibleRouteRows.map((r) => ({ id: r.id, modelPattern: r.modelPattern, routeMode: r.routeMode })),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[visibleRouteRows.map((r) => `${r.id}:${r.modelPattern}:${r.routeMode || 'pattern'}`).join(',')], [routePatternsKey],
); );
const routeBrandById = useMemo(() => { const routeBrandById = useMemo(() => {
@@ -677,56 +691,10 @@ export default function TokenRoutes() {
}) })
), [listVisibleRoutes, sortBy, sortDir]); ), [listVisibleRoutes, sortBy, sortDir]);
const enabledCounts = useMemo(() => { // Shared base filter: all filters EXCEPT enabledFilter
// Count enabled/disabled within the currently filtered scope (excluding the enabled filter itself) const baseFilteredRoutes = useMemo(() => {
let base = sortedRoutes;
if (activeGroupFilter === '__all__') {
base = base.filter((route) => !isRouteExactModel(route));
} else if (typeof activeGroupFilter === 'number') {
base = base.filter((route) => route.id === activeGroupFilter);
}
if (activeBrand) {
if (activeBrand === '__other__') {
base = base.filter((route) => !(routeBrandById.get(route.id) || null));
} else {
base = base.filter((route) => (routeBrandById.get(route.id)?.name || '') === activeBrand);
}
}
if (activeSite) {
base = base.filter((route) => route.siteNames?.includes(activeSite));
}
if (activeEndpointType) {
base = base.filter((route) => (routeEndpointTypesByRouteId[route.id] || new Set<string>()).has(activeEndpointType));
}
if (search.trim()) {
const q = search.trim().toLowerCase();
base = base.filter((route) => {
const modelPattern = route.modelPattern.toLowerCase();
const displayName = (route.displayName || '').toLowerCase();
const title = resolveRouteTitle(route).toLowerCase();
return modelPattern.includes(q) || displayName.includes(q) || title.includes(q);
});
}
let enabled = 0;
let disabled = 0;
for (const route of base) {
if (route.enabled) enabled++;
else disabled++;
}
return { enabled, disabled };
}, [sortedRoutes, activeGroupFilter, activeBrand, activeSite, activeEndpointType, search, routeBrandById, routeEndpointTypesByRouteId]);
const filteredRoutes = useMemo(() => {
let list = sortedRoutes; let list = sortedRoutes;
if (enabledFilter === 'enabled') {
list = list.filter((route) => route.enabled);
} else if (enabledFilter === 'disabled') {
list = list.filter((route) => !route.enabled);
}
if (activeGroupFilter === '__all__') { if (activeGroupFilter === '__all__') {
list = list.filter((route) => !isRouteExactModel(route)); list = list.filter((route) => !isRouteExactModel(route));
} else if (typeof activeGroupFilter === 'number') { } else if (typeof activeGroupFilter === 'number') {
@@ -742,9 +710,7 @@ export default function TokenRoutes() {
} }
if (activeSite) { if (activeSite) {
list = list.filter((route) => list = list.filter((route) => route.siteNames?.includes(activeSite));
route.siteNames?.includes(activeSite),
);
} }
if (activeEndpointType) { if (activeEndpointType) {
@@ -764,17 +730,23 @@ export default function TokenRoutes() {
} }
return list; return list;
}, [ }, [sortedRoutes, activeGroupFilter, activeBrand, activeSite, activeEndpointType, search, routeBrandById, routeEndpointTypesByRouteId]);
sortedRoutes,
enabledFilter, const enabledCounts = useMemo(() => {
activeGroupFilter, let enabled = 0;
activeBrand, let disabled = 0;
activeSite, for (const route of baseFilteredRoutes) {
activeEndpointType, if (route.enabled) enabled++;
search, else disabled++;
routeBrandById, }
routeEndpointTypesByRouteId, return { enabled, disabled };
]); }, [baseFilteredRoutes]);
const filteredRoutes = useMemo(() => {
if (enabledFilter === 'all') return baseFilteredRoutes;
if (enabledFilter === 'enabled') return baseFilteredRoutes.filter((route) => route.enabled);
return baseFilteredRoutes.filter((route) => !route.enabled);
}, [baseFilteredRoutes, enabledFilter]);
useEffect(() => { useEffect(() => {
setVisibleRouteCount(getInitialVisibleCount(filteredRoutes.length, ROUTE_RENDER_CHUNK)); setVisibleRouteCount(getInitialVisibleCount(filteredRoutes.length, ROUTE_RENDER_CHUNK));
@@ -806,7 +778,7 @@ export default function TokenRoutes() {
// Lazy per-route candidate index — only computes for routes actually accessed // 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 candidateIndexCacheRef = useRef<{ key: string; cache: Map<number, RouteCandidateView> }>({ key: '', cache: new Map() });
const candidateIndexCacheKey = `${routePatterns.length}:${Object.keys(modelCandidates).length}`; const candidateIndexCacheKey = `${routePatternsKey}|${Object.keys(modelCandidates).length}`;
if (candidateIndexCacheRef.current.key !== candidateIndexCacheKey) { if (candidateIndexCacheRef.current.key !== candidateIndexCacheKey) {
candidateIndexCacheRef.current = { key: candidateIndexCacheKey, cache: new Map() }; candidateIndexCacheRef.current = { key: candidateIndexCacheKey, cache: new Map() };
} }
@@ -825,7 +797,7 @@ export default function TokenRoutes() {
// Lazy per-route missing token index // Lazy per-route missing token index
const missingTokenCacheRef = useRef<{ key: string; cache: Map<number, RouteMissingTokenHint[]> }>({ key: '', cache: new Map() }); const missingTokenCacheRef = useRef<{ key: string; cache: Map<number, RouteMissingTokenHint[]> }>({ key: '', cache: new Map() });
const missingTokenCacheKey = `${routePatterns.length}:${Object.keys(missingTokenModelsByName).length}`; const missingTokenCacheKey = `${routePatternsKey}|${Object.keys(missingTokenModelsByName).length}`;
if (missingTokenCacheRef.current.key !== missingTokenCacheKey) { if (missingTokenCacheRef.current.key !== missingTokenCacheKey) {
missingTokenCacheRef.current = { key: missingTokenCacheKey, cache: new Map() }; missingTokenCacheRef.current = { key: missingTokenCacheKey, cache: new Map() };
} }
@@ -844,7 +816,7 @@ export default function TokenRoutes() {
// Lazy per-route missing token group index // Lazy per-route missing token group index
const missingTokenGroupCacheRef = useRef<{ key: string; cache: Map<number, RouteMissingTokenHint[]> }>({ key: '', cache: new Map() }); const missingTokenGroupCacheRef = useRef<{ key: string; cache: Map<number, RouteMissingTokenHint[]> }>({ key: '', cache: new Map() });
const missingTokenGroupCacheKey = `${routePatterns.length}:${Object.keys(missingTokenGroupModelsByName).length}`; const missingTokenGroupCacheKey = `${routePatternsKey}|${Object.keys(missingTokenGroupModelsByName).length}`;
if (missingTokenGroupCacheRef.current.key !== missingTokenGroupCacheKey) { if (missingTokenGroupCacheRef.current.key !== missingTokenGroupCacheKey) {
missingTokenGroupCacheRef.current = { key: missingTokenGroupCacheKey, cache: new Map() }; missingTokenGroupCacheRef.current = { key: missingTokenGroupCacheKey, cache: new Map() };
} }
@@ -1026,7 +998,7 @@ export default function TokenRoutes() {
dialog.innerHTML = ` dialog.innerHTML = `
<div style="font-weight:600;font-size:15px;margin-bottom:12px">确认站点屏蔽</div> <div style="font-weight:600;font-size:15px;margin-bottom:12px">确认站点屏蔽</div>
<div style="font-size:13px;color:var(--color-text-secondary);line-height:1.6;margin-bottom:16px"> <div style="font-size:13px;color:var(--color-text-secondary);line-height:1.6;margin-bottom:16px">
将模型「<b>${modelName}</b>」加入站点「<b>${siteName}</b>」的禁用列表。<br/>执行后将自动触发路由重建,该站点下此模型的通道将不再生成。 将模型「<b>${escapeHtml(modelName)}</b>」加入站点「<b>${escapeHtml(siteName)}</b>」的禁用列表。<br/>执行后将自动触发路由重建,该站点下此模型的通道将不再生成。
</div> </div>
<div style="display:flex;justify-content:flex-end;gap:8px"> <div style="display:flex;justify-content:flex-end;gap:8px">
<button id="__sb_cancel" class="btn btn-ghost" style="padding:6px 16px">取消</button> <button id="__sb_cancel" class="btn btn-ghost" style="padding:6px 16px">取消</button>
@@ -1513,8 +1485,8 @@ export default function TokenRoutes() {
onSaveToken={stableChannelTokenSave} onSaveToken={stableChannelTokenSave}
onDeleteChannel={stableDeleteChannel} onDeleteChannel={stableDeleteChannel}
onChannelDragEnd={stableChannelDragEnd} onChannelDragEnd={stableChannelDragEnd}
missingTokenSiteItems={missingTokenSiteItemsByRouteId[route.id] || EMPTY_MISSING_ITEMS} missingTokenSiteItems={getMissingTokenSiteItems(route.id)}
missingTokenGroupItems={missingTokenGroupItemsByRouteId[route.id] || EMPTY_MISSING_GROUP_ITEMS} missingTokenGroupItems={getMissingTokenGroupItems(route.id)}
onCreateTokenForMissing={stableCreateTokenForMissing} onCreateTokenForMissing={stableCreateTokenForMissing}
onAddChannel={stableAddChannel} onAddChannel={stableAddChannel}
expandedSourceGroupMap={expandedSourceGroupMap} expandedSourceGroupMap={expandedSourceGroupMap}