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;