forked from boos/BoosAPI
fix: address review issues in brand filter sync, model guard, route counts, and candidate loading
- Sync blockedBrands local state after save to prevent stale chips - Defer getBrandList from blocking Settings bootstrap - Guard model management UI on brandGroups instead of availableModels - Deduplicate single-model add in disabled models - Skip read-only/zero-channel routes in enabledCounts and filter - Add sequence counter to loadCandidates to prevent force race condition - Use stableAddChannel for mobile add-channel button
This commit is contained in:
@@ -424,14 +424,6 @@ export default function Settings() {
|
||||
api.getRoutesLite(),
|
||||
api.getRuntimeDatabaseConfig(),
|
||||
]);
|
||||
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 * * *',
|
||||
@@ -508,6 +500,10 @@ export default function Settings() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// Load brand list in background (non-blocking, best-effort)
|
||||
api.getBrandList()
|
||||
.then((res: any) => setAllBrandNames(Array.isArray(res?.brands) ? res.brands : []))
|
||||
.catch(() => {/* best-effort */});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -834,10 +830,9 @@ export default function Settings() {
|
||||
setSavingBrandFilter(true);
|
||||
try {
|
||||
const res = await api.updateRuntimeSettings({ globalBlockedBrands: blockedBrands });
|
||||
setRuntime((prev) => ({
|
||||
...prev,
|
||||
globalBlockedBrands: Array.isArray(res?.globalBlockedBrands) ? res.globalBlockedBrands : blockedBrands,
|
||||
}));
|
||||
const resolved = Array.isArray(res?.globalBlockedBrands) ? res.globalBlockedBrands : blockedBrands;
|
||||
setRuntime((prev) => ({ ...prev, globalBlockedBrands: resolved }));
|
||||
setBlockedBrands(resolved);
|
||||
toast.success('品牌屏蔽设置已保存');
|
||||
try {
|
||||
await api.rebuildRoutes(false);
|
||||
|
||||
@@ -953,7 +953,7 @@ export default function Sites() {
|
||||
) : (
|
||||
<>
|
||||
{/* Search and brand group controls */}
|
||||
{availableModels.length > 0 && (
|
||||
{brandGroups.length > 0 && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<input
|
||||
placeholder="搜索模型名称..."
|
||||
@@ -1016,7 +1016,7 @@ export default function Sites() {
|
||||
checked={isDisabled}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setDisabledModels((prev) => [...prev, model]);
|
||||
setDisabledModels((prev) => Array.from(new Set([...prev, model])));
|
||||
} else {
|
||||
setDisabledModels((prev) => prev.filter((m) => m !== model));
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@ export default function TokenRoutes() {
|
||||
const candidatesLoadedRef = useRef(false);
|
||||
const candidatesPromiseRef = useRef<Promise<void> | null>(null);
|
||||
const candidatesVersionRef = useRef(0);
|
||||
const candidatesSeqRef = useRef(0);
|
||||
|
||||
const loadRouteDecisions = async (
|
||||
routeRows: RouteSummaryRow[],
|
||||
@@ -235,10 +236,12 @@ export default function TokenRoutes() {
|
||||
const loadCandidates = (force?: boolean) => {
|
||||
if (candidatesLoadedRef.current && !force) return;
|
||||
if (candidatesPromiseRef.current && !force) return;
|
||||
const seq = ++candidatesSeqRef.current;
|
||||
candidatesLoadedRef.current = true;
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const candidateRows = await api.getModelTokenCandidates();
|
||||
if (candidatesSeqRef.current !== seq) return; // stale
|
||||
startTransition(() => {
|
||||
setModelCandidates((candidateRows?.models || {}) as RouteModelCandidatesByModelName);
|
||||
setMissingTokenModelsByName(
|
||||
@@ -251,7 +254,7 @@ export default function TokenRoutes() {
|
||||
});
|
||||
candidatesVersionRef.current = Date.now();
|
||||
} catch {
|
||||
candidatesLoadedRef.current = false;
|
||||
if (candidatesSeqRef.current === seq) candidatesLoadedRef.current = false;
|
||||
} finally {
|
||||
if (candidatesPromiseRef.current === promise) {
|
||||
candidatesPromiseRef.current = null;
|
||||
@@ -740,6 +743,7 @@ export default function TokenRoutes() {
|
||||
let enabled = 0;
|
||||
let disabled = 0;
|
||||
for (const route of baseFilteredRoutes) {
|
||||
if (route.kind === 'zero_channel' || route.readOnly === true || route.isVirtual === true) continue;
|
||||
if (route.enabled) enabled++;
|
||||
else disabled++;
|
||||
}
|
||||
@@ -748,8 +752,10 @@ export default function TokenRoutes() {
|
||||
|
||||
const filteredRoutes = useMemo(() => {
|
||||
if (enabledFilter === 'all') return baseFilteredRoutes;
|
||||
if (enabledFilter === 'enabled') return baseFilteredRoutes.filter((route) => route.enabled);
|
||||
return baseFilteredRoutes.filter((route) => !route.enabled);
|
||||
return baseFilteredRoutes.filter((route) => {
|
||||
if (route.kind === 'zero_channel' || route.readOnly === true || route.isVirtual === true) return false;
|
||||
return enabledFilter === 'enabled' ? route.enabled : !route.enabled;
|
||||
});
|
||||
}, [baseFilteredRoutes, enabledFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1451,7 +1457,7 @@ export default function TokenRoutes() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link"
|
||||
onClick={() => setAddChannelModalRouteId(route.id)}
|
||||
onClick={() => stableAddChannel(route.id)}
|
||||
>
|
||||
{tr('添加通道')}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user