* feat: Preserve manual models during discovery and refresh, and add Claude configuration files and project documentation.feat: Preserve manual models during discovery and refresh, and add Claude configuration and project documentation files.
* fix: fix Feishu and Larksuite webhook support to the notification service with updated UI and tests.
---------
Co-authored-by: Xiang Li <xiangli>
* fix: MySQL-compatible case-insensitive model merge and disabled model matching
Root cause: MySQL uses utf8mb4_general_ci collation by default, making the
unique index on (account_id, model_name) case-insensitive. When different
tokens discover the same model with different casing (e.g., 'GPT-4o' vs
'gpt-4o'), the case-sensitive JavaScript Set allowed both, but the MySQL
batch INSERT failed with a duplicate key error — causing the entire model
refresh to silently fail and leaving stale routes (including disabled models).
Fixes:
1. mergeDiscoveredModels: change Set<string> to Map<lowercase, original>
for cross-scan dedup, matching normalizeModels' existing behavior.
2. isModelDisabledForSite: compare with toLowerCase() so disabled models
are filtered regardless of case variants across discovery sources.
3. tokens.ts /api/routes/rebuild: add missing await on
rebuildTokenRoutesFromAvailability() so errors propagate correctly
instead of being silently swallowed.
* fix: add missing await on rebuildTokenRoutesFromAvailability in stats.ts
The POST handler for /api/models/check/:accountId called
rebuildTokenRoutesFromAvailability() without await, returning a Promise
object in the response instead of the actual rebuild result.
- 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
Replace closure-captured editor check with loadingModelsSiteIdRef so the
stale-response guard works correctly across rapid site switches. Also
gate setDisabledModelsLoading(false) behind the same ref check.
- 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
- 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
The routes page loaded ~924KB of API data upfront (summary 366KB +
token-candidates 558KB), but token-candidates was only needed when
expanding cards, opening panels, or using filters. This caused a ~3s
element render delay blocking LCP.
Changes:
- Split load() so first paint only fetches routesSummary (366KB)
- Preload token-candidates in background via requestIdleCallback
- Wrap candidates state updates in startTransition to avoid UI jank
- Convert 3 heavy O(routes×models) useMemo indices to lazy per-route
computation with ref-based cache (candidateIndex, missingTokenIndex,
missingTokenGroupIndex)
- Guard 4 ManualRoutePanel-only useMemos with showManual check
- Convert 2 derived useMemos to per-route getter functions
- Add loadCandidates() calls at 7 interaction trigger points
- brandMatcher: port collectBrandCandidates + buildMatchContext from
frontend BrandIcon.tsx so matching is identical between route display
and rebuild filtering; segments now split on /,:,\s only (not - or _),
and candidates are recursively generated from /,:,, splits
- Sites.tsx: remove manual model name input and handleAddDisabledModel;
the checkbox-based batch selection UI fully replaces it