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('筛选路由')}> ) : (