diff --git a/common/metrics/metrics.go b/common/metrics/metrics.go index 58f6784..5ef196c 100644 --- a/common/metrics/metrics.go +++ b/common/metrics/metrics.go @@ -98,6 +98,7 @@ func GetMetrics() *Metrics { return } globalMetrics = NewMetrics(prometheus.DefaultRegisterer) + prometheus.MustRegister(NewChannelCollector()) }) return globalMetrics } diff --git a/router/relay-router.go b/router/relay-router.go index 23c45e1..5a21508 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -79,6 +79,9 @@ func SetRelayRouter(router *gin.Engine) { relayV1Router := router.Group("/v1") relayV1Router.Use(middleware.SystemPerformanceCheck()) relayV1Router.Use(middleware.TokenAuth()) + if metrics.IsEnabled() { + relayV1Router.Use(metrics.MetricsMiddleware(metrics.GetMetrics())) + } relayV1Router.Use(middleware.ModelRequestRateLimit()) relayV1Router.Use(middleware.RelayCaptureMiddleware()) { @@ -165,6 +168,9 @@ func SetRelayRouter(router *gin.Engine) { relayGeminiRouter := router.Group("/v1beta") relayGeminiRouter.Use(middleware.SystemPerformanceCheck()) relayGeminiRouter.Use(middleware.TokenAuth()) + if metrics.IsEnabled() { + relayGeminiRouter.Use(metrics.MetricsMiddleware(metrics.GetMetrics())) + } relayGeminiRouter.Use(middleware.ModelRequestRateLimit()) relayGeminiRouter.Use(middleware.RelayCaptureMiddleware()) relayGeminiRouter.Use(middleware.Distribute()) diff --git a/web/dist/static/login-widget.js b/web/dist/static/login-widget.js deleted file mode 100644 index f3c68c0..0000000 --- a/web/dist/static/login-widget.js +++ /dev/null @@ -1 +0,0 @@ -var NewApiLoginWidget=function(s){"use strict";async function i({apiUrl:r,username:t,password:a}){if(!r)throw new Error("NewApiLoginWidget: apiUrl is required");if(!t||!t.trim())return{success:!1,message:"请输入用户名"};if(!a)return{success:!1,message:"请输入密码"};const n=r.replace(/\/+$/,"")+"/api/user/login";try{const e=await(await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify({username:t.trim(),password:a})})).json();return e.success?e.data&&e.data.require_2fa?{success:!1,message:"此账户启用了两步验证,暂不支持通过 Widget 登录"}:{success:!0,data:e.data}:{success:!1,message:e.message||"登录失败"}}catch{return{success:!1,message:"网络错误,请检查网络连接"}}}return window.NewApiLoginWidget={login:i},s.login=i,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"}),s}({}); diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index ec649f7..7fd78a3 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -1361,8 +1361,8 @@ const EditChannelModal = (props) => { } delete localInputs.vertex_files; - if (!isEdit && (!localInputs.name || !localInputs.public_name || !localInputs.key)) { - showInfo(t('请填写渠道名称、对外名称和渠道密钥!')); + if (!isEdit && (!localInputs.name || !localInputs.key)) { + showInfo(t('请填写渠道名称和渠道密钥!')); return; } if (!Array.isArray(localInputs.models) || localInputs.models.length === 0) { @@ -2014,16 +2014,6 @@ const EditChannelModal = (props) => { autoComplete='new-password' /> - handleInputChange('public_name', value)} - autoComplete='new-password' - /> - {inputs.type === 33 && ( <> - - {text} - {record.channelTags.map((tag, idx) => ( - { allSubscriptions={allSubscriptions} reloadSubscriptionSelf={getSubscriptionSelf} /> - {/* */} + /> ); diff --git a/web/src/hooks/models/useModelsData.jsx b/web/src/hooks/models/useModelsData.jsx index b1bd8b6..401515b 100644 --- a/web/src/hooks/models/useModelsData.jsx +++ b/web/src/hooks/models/useModelsData.jsx @@ -35,6 +35,9 @@ export const useModelsData = () => { const [searching, setSearching] = useState(false); const [modelCount, setModelCount] = useState(0); + // snapshot of the last *submitted* search — distinct from live form values, so pagination stays correct if the user edits the form without re-submitting + const [activeSearch, setActiveSearch] = useState({ keyword: '', vendor: '' }); + // Modal states const [showEdit, setShowEdit] = useState(false); const [editingModel, setEditingModel] = useState({ @@ -250,25 +253,16 @@ export const useModelsData = () => { } }; - // Search models with keyword and vendor - const searchModels = async () => { - const { searchKeyword = '', searchVendor = '' } = getFormValues(); - - if (searchKeyword === '' && searchVendor === '') { - // If keyword is blank, load models instead - await loadModels(1, pageSize); - return; - } - + const fetchSearchPage = async (keyword, vendor, page, size) => { setSearching(true); try { - const res = await API.get( - `/api/models/search?keyword=${searchKeyword}&vendor=${searchVendor}&p=1&page_size=${pageSize}`, - ); + const res = await API.get('/api/models/search', { + params: { keyword, vendor, p: page, page_size: size }, + }); const { success, message, data } = res.data; if (success) { const newPageData = extractItems(data); - setActivePage(data.page || 1); + setActivePage(data.page || page); setModelCount(data.total || newPageData.length); setModelFormat(newPageData); if (data.vendor_counts) { @@ -278,16 +272,36 @@ export const useModelsData = () => { ); setVendorCounts({ ...data.vendor_counts, all: sumAll }); } - } else { - showError(message); - setModels([]); + return true; } + showError(message); + setModels([]); + return false; } catch (error) { console.error(error); showError(t('搜索模型失败')); setModels([]); + return false; + } finally { + setSearching(false); + } + }; + + // Search models with keyword and vendor + const searchModels = async () => { + const { searchKeyword = '', searchVendor = '' } = getFormValues(); + + if (searchKeyword === '' && searchVendor === '') { + // If keyword is blank, load models instead and clear search state + setActiveSearch({ keyword: '', vendor: '' }); + await loadModels(1, pageSize); + return; + } + + const ok = await fetchSearchPage(searchKeyword, searchVendor, 1, pageSize); + if (ok) { + setActiveSearch({ keyword: searchKeyword, vendor: searchVendor }); } - setSearching(false); }; // Manage model (enable/disable/delete) @@ -328,13 +342,18 @@ export const useModelsData = () => { }; // Handle page change - const handlePageChange = (page) => { + const handlePageChange = async (page) => { setActivePage(page); - loadModels(page, pageSize, activeVendorKey); + if (activeSearch.keyword || activeSearch.vendor) { + await fetchSearchPage(activeSearch.keyword, activeSearch.vendor, page, pageSize); + } else { + loadModels(page, pageSize, activeVendorKey); + } }; // Reload models when activeVendorKey changes useEffect(() => { + setActiveSearch({ keyword: '', vendor: '' }); loadModels(1, pageSize, activeVendorKey); }, [activeVendorKey]); @@ -342,7 +361,11 @@ export const useModelsData = () => { const handlePageSizeChange = async (size) => { setPageSize(size); setActivePage(1); - await loadModels(1, size, activeVendorKey); + if (activeSearch.keyword || activeSearch.vendor) { + await fetchSearchPage(activeSearch.keyword, activeSearch.vendor, 1, size); + } else { + await loadModels(1, size, activeVendorKey); + } }; // Handle row click and styling diff --git a/web/src/pages/Home/HomePricingFilters.jsx b/web/src/pages/Home/HomePricingFilters.jsx index 496b8ae..0966940 100644 --- a/web/src/pages/Home/HomePricingFilters.jsx +++ b/web/src/pages/Home/HomePricingFilters.jsx @@ -77,12 +77,12 @@ const HomePricingFilters = ({ t }) => { const vendorItems = useMemo(() => { const models = pricingData.models; - const vendorSet = new Set(); const vendorIcons = new Map(); + const vendorNamesInModels = new Set(); let hasUnknown = false; (models || []).forEach((m) => { if (m.vendor_name) { - vendorSet.add(m.vendor_name); + vendorNamesInModels.add(m.vendor_name); if (m.vendor_icon && !vendorIcons.has(m.vendor_name)) { vendorIcons.set(m.vendor_name, m.vendor_icon); } @@ -90,10 +90,20 @@ const HomePricingFilters = ({ t }) => { hasUnknown = true; } }); - const vendors = Array.from(vendorSet).sort(); + // 按后端 vendorsMap 的 sort_order 顺序排列 + const vMap = pricingData.vendorsMap || {}; + const orderedVendors = Object.values(vMap) + .filter((v) => vendorNamesInModels.has(v.name)) + .map((v) => v.name); + // fallback: vendors with models that are absent from vendorsMap (e.g. not yet registered) + const orderedVendorSet = new Set(orderedVendors); + vendorNamesInModels.forEach((name) => { + if (!orderedVendorSet.has(name)) { + orderedVendors.push(name); + } + }); const getVendorCount = () => { - // 计算供应商数量(包含未知供应商) - const count = vendorSet.size + (hasUnknown ? 1 : 0); + const count = vendorNamesInModels.size + (hasUnknown ? 1 : 0); return count; }; const getModelCount = (v) => { @@ -104,7 +114,7 @@ const HomePricingFilters = ({ t }) => { const items = [ { value: 'all', label: t('全部供应商'), tagCount: getVendorCount(), disabled: (models || []).length === 0 }, ]; - vendors.forEach((v) => { + orderedVendors.forEach((v) => { const count = getModelCount(v); items.push({ value: v, @@ -123,7 +133,7 @@ const HomePricingFilters = ({ t }) => { }); } return items; - }, [pricingData.models, vendorModels, t]); + }, [pricingData.models, pricingData.vendorsMap, vendorModels, t]); const typeItems = useMemo(() => { const models = pricingData.models;