feat: auto fetch upstream models (#2979)

* feat: add upstream model update detection with scheduled sync and manual apply flows

* feat: support upstream model removal sync and selectable deletes in update modal

* feat: add detect-only upstream updates and show compact +/- model badges

* feat: improve upstream model update UX

* feat: improve upstream model update UX

* fix: respect model_mapping in upstream update detection

* feat: improve upstream update modal to prevent missed add/remove actions

* feat: add admin upstream model update notifications with digest and truncation

* fix: avoid repeated partial-submit confirmation in upstream update modal

* feat: improve ui/ux

* feat: suppress upstream update alerts for unchanged channel-count within 24h

* fix: submit upstream update choices even when no models are selected

* feat: improve upstream model update flow and split frontend updater

* fix merge conflict
This commit is contained in:
Seefs
2026-03-02 22:01:53 +08:00
committed by GitHub
parent d36f4205a9
commit e71f5a45f2
22 changed files with 2422 additions and 305 deletions

View File

@@ -37,8 +37,13 @@ import {
renderQuotaWithAmount,
showSuccess,
showError,
showInfo,
} from '../../../helpers';
import { CHANNEL_OPTIONS } from '../../../constants';
import {
CHANNEL_OPTIONS,
MODEL_FETCHABLE_CHANNEL_TYPES,
} from '../../../constants';
import { parseUpstreamUpdateMeta } from '../../../hooks/channels/upstreamUpdateUtils';
import {
IconTreeTriangleDown,
IconMore,
@@ -270,6 +275,35 @@ const isRequestPassThroughEnabled = (record) => {
}
};
const getUpstreamUpdateMeta = (record) => {
const supported =
!!record &&
record.children === undefined &&
MODEL_FETCHABLE_CHANNEL_TYPES.has(record.type);
if (!record || record.children !== undefined) {
return {
supported: false,
enabled: false,
pendingAddModels: [],
pendingRemoveModels: [],
};
}
const parsed =
record?.upstreamUpdateMeta && typeof record.upstreamUpdateMeta === 'object'
? record.upstreamUpdateMeta
: parseUpstreamUpdateMeta(record?.settings);
return {
supported,
enabled: parsed?.enabled === true,
pendingAddModels: Array.isArray(parsed?.pendingAddModels)
? parsed.pendingAddModels
: [],
pendingRemoveModels: Array.isArray(parsed?.pendingRemoveModels)
? parsed.pendingRemoveModels
: [],
};
};
export const getChannelsColumns = ({
t,
COLUMN_KEYS,
@@ -291,6 +325,8 @@ export const getChannelsColumns = ({
checkOllamaVersion,
setShowMultiKeyManageModal,
setCurrentMultiKeyChannel,
openUpstreamUpdateModal,
detectChannelUpstreamUpdates,
}) => {
return [
{
@@ -304,6 +340,14 @@ export const getChannelsColumns = ({
dataIndex: 'name',
render: (text, record, index) => {
const passThroughEnabled = isRequestPassThroughEnabled(record);
const upstreamUpdateMeta = getUpstreamUpdateMeta(record);
const pendingAddCount = upstreamUpdateMeta.pendingAddModels.length;
const pendingRemoveCount =
upstreamUpdateMeta.pendingRemoveModels.length;
const showUpstreamUpdateTag =
upstreamUpdateMeta.supported &&
upstreamUpdateMeta.enabled &&
(pendingAddCount > 0 || pendingRemoveCount > 0);
const nameNode =
record.remark && record.remark.trim() !== '' ? (
<Tooltip
@@ -339,26 +383,76 @@ export const getChannelsColumns = ({
<span>{text}</span>
);
if (!passThroughEnabled) {
if (!passThroughEnabled && !showUpstreamUpdateTag) {
return nameNode;
}
return (
<Space spacing={6} align='center'>
{nameNode}
<Tooltip
content={t(
'该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。',
)}
trigger='hover'
position='topLeft'
>
<span className='inline-flex items-center'>
<IconAlertTriangle
style={{ color: 'var(--semi-color-warning)' }}
/>
</span>
</Tooltip>
{passThroughEnabled && (
<Tooltip
content={t(
'该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。',
)}
trigger='hover'
position='topLeft'
>
<span className='inline-flex items-center'>
<IconAlertTriangle
style={{ color: 'var(--semi-color-warning)' }}
/>
</span>
</Tooltip>
)}
{showUpstreamUpdateTag && (
<Space spacing={4} align='center'>
{pendingAddCount > 0 ? (
<Tooltip content={t('点击处理新增模型')} position='top'>
<Tag
color='green'
type='light'
size='small'
shape='circle'
className='cursor-pointer transition-all duration-150 hover:opacity-85 hover:-translate-y-px active:scale-95'
onClick={(e) => {
e.stopPropagation();
openUpstreamUpdateModal(
record,
upstreamUpdateMeta.pendingAddModels,
upstreamUpdateMeta.pendingRemoveModels,
'add',
);
}}
>
+{pendingAddCount}
</Tag>
</Tooltip>
) : null}
{pendingRemoveCount > 0 ? (
<Tooltip content={t('点击处理删除模型')} position='top'>
<Tag
color='red'
type='light'
size='small'
shape='circle'
className='cursor-pointer transition-all duration-150 hover:opacity-85 hover:-translate-y-px active:scale-95'
onClick={(e) => {
e.stopPropagation();
openUpstreamUpdateModal(
record,
upstreamUpdateMeta.pendingAddModels,
upstreamUpdateMeta.pendingRemoveModels,
'remove',
);
}}
>
-{pendingRemoveCount}
</Tag>
</Tooltip>
) : null}
</Space>
)}
</Space>
);
},
@@ -585,6 +679,7 @@ export const getChannelsColumns = ({
fixed: 'right',
render: (text, record, index) => {
if (record.children === undefined) {
const upstreamUpdateMeta = getUpstreamUpdateMeta(record);
const moreMenuItems = [
{
node: 'item',
@@ -622,6 +717,47 @@ export const getChannelsColumns = ({
},
];
if (upstreamUpdateMeta.supported) {
moreMenuItems.push({
node: 'item',
name: t('仅检测上游模型更新'),
type: 'tertiary',
onClick: () => {
if (!upstreamUpdateMeta.enabled) {
showInfo(t('该渠道未开启上游模型更新检测'));
return;
}
detectChannelUpstreamUpdates(record);
},
});
moreMenuItems.push({
node: 'item',
name: t('处理上游模型更新'),
type: 'tertiary',
onClick: () => {
if (!upstreamUpdateMeta.enabled) {
showInfo(t('该渠道未开启上游模型更新检测'));
return;
}
if (
upstreamUpdateMeta.pendingAddModels.length === 0 &&
upstreamUpdateMeta.pendingRemoveModels.length === 0
) {
showInfo(t('该渠道暂无可处理的上游模型更新'));
return;
}
openUpstreamUpdateModal(
record,
upstreamUpdateMeta.pendingAddModels,
upstreamUpdateMeta.pendingRemoveModels,
upstreamUpdateMeta.pendingAddModels.length > 0
? 'add'
: 'remove',
);
},
});
}
if (record.type === 4) {
moreMenuItems.unshift({
node: 'item',