From 26be7c024a627483c1831817f8869491e511d567 Mon Sep 17 00:00:00 2001
From: Hureru <3507039083@qq.com>
Date: Sun, 22 Mar 2026 20:16:29 +0800
Subject: [PATCH] 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
---
src/server/routes/api/settings.ts | 31 +++++++++++++++++++++++++----
src/server/routes/api/sites.ts | 6 +++---
src/server/services/brandMatcher.ts | 2 +-
src/web/pages/Settings.tsx | 14 ++++++++++---
src/web/pages/Sites.tsx | 17 ++++++++++++----
src/web/pages/TokenRoutes.tsx | 19 +++++++++++++-----
6 files changed, 69 insertions(+), 20 deletions(-)
diff --git a/src/server/routes/api/settings.ts b/src/server/routes/api/settings.ts
index f7c5fd9..23af343 100644
--- a/src/server/routes/api/settings.ts
+++ b/src/server/routes/api/settings.ts
@@ -401,7 +401,19 @@ function applyImportedSettingToRuntime(key: string, value: unknown) {
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
if (Array.isArray(parsed)) {
- config.globalBlockedBrands = parsed.filter((b): b is string => typeof b === 'string').map((b) => b.trim()).filter(Boolean);
+ const nextBrands = parsed.filter((b): b is string => typeof b === 'string').map((b) => b.trim()).filter(Boolean);
+ const prev = JSON.stringify(config.globalBlockedBrands);
+ config.globalBlockedBrands = nextBrands;
+ if (prev !== JSON.stringify(nextBrands)) {
+ startBackgroundTask(
+ {
+ type: 'maintenance',
+ title: '品牌屏蔽变更后重建路由',
+ dedupeKey: 'refresh-models-and-rebuild-routes',
+ },
+ async () => refreshModelsAndRebuildRoutes(),
+ );
+ }
}
} catch {
return;
@@ -965,9 +977,10 @@ export async function settingsRoutes(app: FastifyInstance) {
}
if (body.globalBlockedBrands !== undefined) {
- const nextBrands = Array.isArray(body.globalBlockedBrands)
- ? body.globalBlockedBrands.filter((b): b is string => typeof b === 'string').map((b) => b.trim()).filter(Boolean)
- : [];
+ if (!Array.isArray(body.globalBlockedBrands)) {
+ return reply.code(400).send({ error: 'globalBlockedBrands must be an array of strings' });
+ }
+ const nextBrands = body.globalBlockedBrands.filter((b): b is string => typeof b === 'string').map((b) => b.trim()).filter(Boolean);
const uniqueBrands = Array.from(new Set(nextBrands));
const prev = JSON.stringify(config.globalBlockedBrands);
const next = JSON.stringify(uniqueBrands);
@@ -976,6 +989,16 @@ export async function settingsRoutes(app: FastifyInstance) {
}
config.globalBlockedBrands = uniqueBrands;
upsertSetting('global_blocked_brands', JSON.stringify(uniqueBrands));
+ if (prev !== next) {
+ startBackgroundTask(
+ {
+ type: 'maintenance',
+ title: '品牌屏蔽变更后重建路由',
+ dedupeKey: 'refresh-models-and-rebuild-routes',
+ },
+ async () => refreshModelsAndRebuildRoutes(),
+ );
+ }
}
if (body.webhookUrl !== undefined) {
diff --git a/src/server/routes/api/sites.ts b/src/server/routes/api/sites.ts
index 0fdf23d..ee9fddd 100644
--- a/src/server/routes/api/sites.ts
+++ b/src/server/routes/api/sites.ts
@@ -534,9 +534,9 @@ export async function sitesRoutes(app: FastifyInstance) {
.all();
const models = Array.from(new Set([
- ...accountModels.map((r) => r.modelName),
- ...tokenModels.map((r) => r.modelName),
- ])).filter((m) => m.trim().length > 0).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
+ ...accountModels.map((r) => r.modelName.trim()),
+ ...tokenModels.map((r) => r.modelName.trim()),
+ ])).filter((m) => m.length > 0).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
return { siteId: id, models };
});
diff --git a/src/server/services/brandMatcher.ts b/src/server/services/brandMatcher.ts
index efca07d..f4a93ac 100644
--- a/src/server/services/brandMatcher.ts
+++ b/src/server/services/brandMatcher.ts
@@ -242,7 +242,7 @@ function matchesRule(ctx: MatchContext, rule: KeywordRule): boolean {
case 'segment':
return ctx.segments.includes(rule.keyword);
case 'boundary': {
- const pattern = new RegExp(`(^|[^a-z0-9])${rule.keyword}(?=$|[^a-z0-9])`);
+ const pattern = new RegExp(`(^|[/:_\\-\\s])${rule.keyword}(?=$|[/:_\\-\\s])`);
return pattern.test(ctx.raw) || ctx.candidates.some((c) => pattern.test(c));
}
default:
diff --git a/src/web/pages/Settings.tsx b/src/web/pages/Settings.tsx
index 65950dd..5a2f953 100644
--- a/src/web/pages/Settings.tsx
+++ b/src/web/pages/Settings.tsx
@@ -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() {
setShowFilters(false)} title={tr('筛选路由')}>
) : (