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:
56
web/src/hooks/channels/upstreamUpdateUtils.js
vendored
Normal file
56
web/src/hooks/channels/upstreamUpdateUtils.js
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
export const normalizeModelList = (models = []) =>
|
||||
Array.from(
|
||||
new Set(
|
||||
(models || []).map((model) => String(model || '').trim()).filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
export const parseUpstreamUpdateMeta = (settings) => {
|
||||
let parsed = null;
|
||||
if (settings && typeof settings === 'object') {
|
||||
parsed = settings;
|
||||
} else if (typeof settings === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(settings);
|
||||
} catch (error) {
|
||||
parsed = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return {
|
||||
enabled: false,
|
||||
pendingAddModels: [],
|
||||
pendingRemoveModels: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: parsed.upstream_model_update_check_enabled === true,
|
||||
pendingAddModels: normalizeModelList(
|
||||
parsed.upstream_model_update_last_detected_models,
|
||||
),
|
||||
pendingRemoveModels: normalizeModelList(
|
||||
parsed.upstream_model_update_last_removed_models,
|
||||
),
|
||||
};
|
||||
};
|
||||
288
web/src/hooks/channels/useChannelUpstreamUpdates.jsx
vendored
Normal file
288
web/src/hooks/channels/useChannelUpstreamUpdates.jsx
vendored
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { API, showError, showInfo, showSuccess } from '../../helpers';
|
||||
import { normalizeModelList } from './upstreamUpdateUtils';
|
||||
|
||||
export const useChannelUpstreamUpdates = ({ t, refresh }) => {
|
||||
const [showUpstreamUpdateModal, setShowUpstreamUpdateModal] = useState(false);
|
||||
const [upstreamUpdateChannel, setUpstreamUpdateChannel] = useState(null);
|
||||
const [upstreamUpdateAddModels, setUpstreamUpdateAddModels] = useState([]);
|
||||
const [upstreamUpdateRemoveModels, setUpstreamUpdateRemoveModels] = useState(
|
||||
[],
|
||||
);
|
||||
const [upstreamUpdatePreferredTab, setUpstreamUpdatePreferredTab] =
|
||||
useState('add');
|
||||
const [upstreamApplyLoading, setUpstreamApplyLoading] = useState(false);
|
||||
const [detectAllUpstreamUpdatesLoading, setDetectAllUpstreamUpdatesLoading] =
|
||||
useState(false);
|
||||
const [applyAllUpstreamUpdatesLoading, setApplyAllUpstreamUpdatesLoading] =
|
||||
useState(false);
|
||||
|
||||
const applyUpstreamUpdatesInFlightRef = useRef(false);
|
||||
const detectChannelUpstreamUpdatesInFlightRef = useRef(false);
|
||||
const detectAllUpstreamUpdatesInFlightRef = useRef(false);
|
||||
const applyAllUpstreamUpdatesInFlightRef = useRef(false);
|
||||
|
||||
const openUpstreamUpdateModal = (
|
||||
record,
|
||||
pendingAddModels = [],
|
||||
pendingRemoveModels = [],
|
||||
preferredTab = 'add',
|
||||
) => {
|
||||
const normalizedAddModels = normalizeModelList(pendingAddModels);
|
||||
const normalizedRemoveModels = normalizeModelList(pendingRemoveModels);
|
||||
if (
|
||||
!record?.id ||
|
||||
(normalizedAddModels.length === 0 && normalizedRemoveModels.length === 0)
|
||||
) {
|
||||
showInfo(t('该渠道暂无可处理的上游模型更新'));
|
||||
return;
|
||||
}
|
||||
setUpstreamUpdateChannel(record);
|
||||
setUpstreamUpdateAddModels(normalizedAddModels);
|
||||
setUpstreamUpdateRemoveModels(normalizedRemoveModels);
|
||||
const normalizedPreferredTab = preferredTab === 'remove' ? 'remove' : 'add';
|
||||
setUpstreamUpdatePreferredTab(normalizedPreferredTab);
|
||||
setShowUpstreamUpdateModal(true);
|
||||
};
|
||||
|
||||
const closeUpstreamUpdateModal = () => {
|
||||
setShowUpstreamUpdateModal(false);
|
||||
setUpstreamUpdateChannel(null);
|
||||
setUpstreamUpdateAddModels([]);
|
||||
setUpstreamUpdateRemoveModels([]);
|
||||
setUpstreamUpdatePreferredTab('add');
|
||||
};
|
||||
|
||||
const applyUpstreamUpdates = async ({
|
||||
addModels: selectedAddModels = [],
|
||||
removeModels: selectedRemoveModels = [],
|
||||
} = {}) => {
|
||||
if (applyUpstreamUpdatesInFlightRef.current) {
|
||||
showInfo(t('正在处理,请稍候'));
|
||||
return;
|
||||
}
|
||||
if (!upstreamUpdateChannel?.id) {
|
||||
closeUpstreamUpdateModal();
|
||||
return;
|
||||
}
|
||||
applyUpstreamUpdatesInFlightRef.current = true;
|
||||
setUpstreamApplyLoading(true);
|
||||
|
||||
try {
|
||||
const normalizedSelectedAddModels = normalizeModelList(selectedAddModels);
|
||||
const normalizedSelectedRemoveModels =
|
||||
normalizeModelList(selectedRemoveModels);
|
||||
const selectedAddSet = new Set(normalizedSelectedAddModels);
|
||||
const ignoreModels = upstreamUpdateAddModels.filter(
|
||||
(model) => !selectedAddSet.has(model),
|
||||
);
|
||||
|
||||
const res = await API.post(
|
||||
'/api/channel/upstream_updates/apply',
|
||||
{
|
||||
id: upstreamUpdateChannel.id,
|
||||
add_models: normalizedSelectedAddModels,
|
||||
ignore_models: ignoreModels,
|
||||
remove_models: normalizedSelectedRemoveModels,
|
||||
},
|
||||
{ skipErrorHandler: true },
|
||||
);
|
||||
const { success, message, data } = res.data || {};
|
||||
if (!success) {
|
||||
showError(message || t('操作失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
const addedCount = data?.added_models?.length || 0;
|
||||
const removedCount = data?.removed_models?.length || 0;
|
||||
const ignoredCount = data?.ignored_models?.length || 0;
|
||||
showSuccess(
|
||||
t(
|
||||
'已处理上游模型更新:加入 {{added}} 个,删除 {{removed}} 个,忽略 {{ignored}} 个',
|
||||
{
|
||||
added: addedCount,
|
||||
removed: removedCount,
|
||||
ignored: ignoredCount,
|
||||
},
|
||||
),
|
||||
);
|
||||
closeUpstreamUpdateModal();
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
showError(
|
||||
error?.response?.data?.message || error?.message || t('操作失败'),
|
||||
);
|
||||
} finally {
|
||||
applyUpstreamUpdatesInFlightRef.current = false;
|
||||
setUpstreamApplyLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyAllUpstreamUpdates = async () => {
|
||||
if (applyAllUpstreamUpdatesInFlightRef.current) {
|
||||
showInfo(t('正在批量处理,请稍候'));
|
||||
return;
|
||||
}
|
||||
applyAllUpstreamUpdatesInFlightRef.current = true;
|
||||
setApplyAllUpstreamUpdatesLoading(true);
|
||||
try {
|
||||
const res = await API.post(
|
||||
'/api/channel/upstream_updates/apply_all',
|
||||
{},
|
||||
{ skipErrorHandler: true },
|
||||
);
|
||||
const { success, message, data } = res.data || {};
|
||||
if (!success) {
|
||||
showError(message || t('批量处理失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
const channelCount = data?.processed_channels || 0;
|
||||
const addedCount = data?.added_models || 0;
|
||||
const removedCount = data?.removed_models || 0;
|
||||
const failedCount = (data?.failed_channel_ids || []).length;
|
||||
showSuccess(
|
||||
t(
|
||||
'已批量处理上游模型更新:渠道 {{channels}} 个,加入 {{added}} 个,删除 {{removed}} 个,失败 {{fails}} 个',
|
||||
{
|
||||
channels: channelCount,
|
||||
added: addedCount,
|
||||
removed: removedCount,
|
||||
fails: failedCount,
|
||||
},
|
||||
),
|
||||
);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
showError(
|
||||
error?.response?.data?.message || error?.message || t('批量处理失败'),
|
||||
);
|
||||
} finally {
|
||||
applyAllUpstreamUpdatesInFlightRef.current = false;
|
||||
setApplyAllUpstreamUpdatesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const detectChannelUpstreamUpdates = async (channel) => {
|
||||
if (detectChannelUpstreamUpdatesInFlightRef.current) {
|
||||
showInfo(t('正在检测,请稍候'));
|
||||
return;
|
||||
}
|
||||
if (!channel?.id) {
|
||||
return;
|
||||
}
|
||||
detectChannelUpstreamUpdatesInFlightRef.current = true;
|
||||
try {
|
||||
const res = await API.post(
|
||||
'/api/channel/upstream_updates/detect',
|
||||
{
|
||||
id: channel.id,
|
||||
},
|
||||
{ skipErrorHandler: true },
|
||||
);
|
||||
const { success, message, data } = res.data || {};
|
||||
if (!success) {
|
||||
showError(message || t('检测失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
const addCount = data?.add_models?.length || 0;
|
||||
const removeCount = data?.remove_models?.length || 0;
|
||||
showSuccess(
|
||||
t('检测完成:新增 {{add}} 个,删除 {{remove}} 个', {
|
||||
add: addCount,
|
||||
remove: removeCount,
|
||||
}),
|
||||
);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
showError(
|
||||
error?.response?.data?.message || error?.message || t('检测失败'),
|
||||
);
|
||||
} finally {
|
||||
detectChannelUpstreamUpdatesInFlightRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const detectAllUpstreamUpdates = async () => {
|
||||
if (detectAllUpstreamUpdatesInFlightRef.current) {
|
||||
showInfo(t('正在批量检测,请稍候'));
|
||||
return;
|
||||
}
|
||||
detectAllUpstreamUpdatesInFlightRef.current = true;
|
||||
setDetectAllUpstreamUpdatesLoading(true);
|
||||
try {
|
||||
const res = await API.post(
|
||||
'/api/channel/upstream_updates/detect_all',
|
||||
{},
|
||||
{ skipErrorHandler: true },
|
||||
);
|
||||
const { success, message, data } = res.data || {};
|
||||
if (!success) {
|
||||
showError(message || t('批量检测失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
const channelCount = data?.processed_channels || 0;
|
||||
const addCount = data?.detected_add_models || 0;
|
||||
const removeCount = data?.detected_remove_models || 0;
|
||||
const failedCount = (data?.failed_channel_ids || []).length;
|
||||
showSuccess(
|
||||
t(
|
||||
'批量检测完成:渠道 {{channels}} 个,新增 {{add}} 个,删除 {{remove}} 个,失败 {{fails}} 个',
|
||||
{
|
||||
channels: channelCount,
|
||||
add: addCount,
|
||||
remove: removeCount,
|
||||
fails: failedCount,
|
||||
},
|
||||
),
|
||||
);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
showError(
|
||||
error?.response?.data?.message || error?.message || t('批量检测失败'),
|
||||
);
|
||||
} finally {
|
||||
detectAllUpstreamUpdatesInFlightRef.current = false;
|
||||
setDetectAllUpstreamUpdatesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
showUpstreamUpdateModal,
|
||||
setShowUpstreamUpdateModal,
|
||||
upstreamUpdateChannel,
|
||||
upstreamUpdateAddModels,
|
||||
upstreamUpdateRemoveModels,
|
||||
upstreamUpdatePreferredTab,
|
||||
upstreamApplyLoading,
|
||||
detectAllUpstreamUpdatesLoading,
|
||||
applyAllUpstreamUpdatesLoading,
|
||||
openUpstreamUpdateModal,
|
||||
closeUpstreamUpdateModal,
|
||||
applyUpstreamUpdates,
|
||||
applyAllUpstreamUpdates,
|
||||
detectChannelUpstreamUpdates,
|
||||
detectAllUpstreamUpdates,
|
||||
};
|
||||
};
|
||||
8
web/src/hooks/channels/useChannelsData.jsx
vendored
8
web/src/hooks/channels/useChannelsData.jsx
vendored
@@ -35,6 +35,8 @@ import {
|
||||
} from '../../constants';
|
||||
import { useIsMobile } from '../common/useIsMobile';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
import { useChannelUpstreamUpdates } from './useChannelUpstreamUpdates';
|
||||
import { parseUpstreamUpdateMeta } from './upstreamUpdateUtils';
|
||||
import { Modal, Button } from '@douyinfe/semi-ui';
|
||||
import { openCodexUsageModal } from '../../components/table/channels/modals/CodexUsageModal';
|
||||
|
||||
@@ -235,6 +237,9 @@ export const useChannelsData = () => {
|
||||
let channelTags = {};
|
||||
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
channels[i].upstreamUpdateMeta = parseUpstreamUpdateMeta(
|
||||
channels[i].settings,
|
||||
);
|
||||
channels[i].key = '' + channels[i].id;
|
||||
if (!enableTagMode) {
|
||||
channelDates.push(channels[i]);
|
||||
@@ -432,6 +437,8 @@ export const useChannelsData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const upstreamUpdates = useChannelUpstreamUpdates({ t, refresh });
|
||||
|
||||
// Channel management
|
||||
const manageChannel = async (id, action, record, value) => {
|
||||
let data = { id };
|
||||
@@ -1194,6 +1201,7 @@ export const useChannelsData = () => {
|
||||
setShowMultiKeyManageModal,
|
||||
currentMultiKeyChannel,
|
||||
setCurrentMultiKeyChannel,
|
||||
...upstreamUpdates,
|
||||
|
||||
// Form
|
||||
formApi,
|
||||
|
||||
Reference in New Issue
Block a user