feat(models-sync): official upstream sync with conflict resolution UI, opt‑out flag, and backend resiliency

Backend
- Add endpoints:
  - GET /api/models/sync_upstream/preview — diff preview (filters out models with sync_official = 0)
  - POST /api/models/sync_upstream — apply sync (create missing; optionally overwrite selected fields)
- Respect opt‑out: skip models with sync_official = 0 in both preview and apply
- Return detailed stats: created_models, created_vendors, updated_models, skipped_models, plus created_list / updated_list
- Add model.Model.SyncOfficial (default 1); auto‑migrated by GORM
- Make HTTP fetching robust:
  - Shared http.Client (connection reuse) with 3x exponential backoff retry
  - 10MB response cap; keep existing IPv4‑first for *.github.io
- Vendor handling:
  - New ensureVendorID helper (cache lookup → DB lookup → create), reduces round‑trips
  - Transactional overwrite to avoid partial updates
- Small cleanups and clearer helpers (containsField, coalesce, chooseStatus)

Frontend
- ModelsActions: add “Sync official” button with Popover (p‑2) explaining community contribution; loading = syncing || previewing; preview → conflict modal → apply flow
- New UpstreamConflictModal:
  - Per‑field columns (description/icon/tags/vendor/name_rule/status) with column‑level checkbox to select all
  - Cell with Checkbox + Tag (“Click to view differences”) and Popover (p‑2) showing Local vs Official values
  - Auto‑hide columns with no conflicts; responsive width; use native Semi Modal footer
  - Full i18n coverage
- useModelsData: add syncing/previewing states; new methods previewUpstreamDiff, applyUpstreamOverwrite, syncUpstream; refresh vendors/models after apply
- EditModelModal: add “Participate in official sync” switch; persisted as sync_official
- ModelsColumnDefs: add “Participate in official sync” column

i18n
- Add missing English keys for the new UI and messages; fix quoting issues

Refs
- Upstream metadata: https://github.com/basellm/llm-metadata
This commit is contained in:
t0ng7u
2025-09-02 02:04:22 +08:00
parent 55c8271311
commit 54f118d9ba
10 changed files with 902 additions and 22 deletions
@@ -21,10 +21,11 @@ import React, { useState } from 'react';
import MissingModelsModal from './modals/MissingModelsModal';
import PrefillGroupManagement from './modals/PrefillGroupManagement';
import EditPrefillGroupModal from './modals/EditPrefillGroupModal';
import { Button, Modal } from '@douyinfe/semi-ui';
import { Button, Modal, Popover } from '@douyinfe/semi-ui';
import { showSuccess, showError, copy } from '../../../helpers';
import CompactModeToggle from '../../common/ui/CompactModeToggle';
import SelectionNotification from './components/SelectionNotification';
import UpstreamConflictModal from './modals/UpstreamConflictModal';
const ModelsActions = ({
selectedKeys,
@@ -32,6 +33,11 @@ const ModelsActions = ({
setEditingModel,
setShowEdit,
batchDeleteModels,
syncing,
previewing,
syncUpstream,
previewUpstreamDiff,
applyUpstreamOverwrite,
compactMode,
setCompactMode,
t,
@@ -42,6 +48,21 @@ const ModelsActions = ({
const [showGroupManagement, setShowGroupManagement] = useState(false);
const [showAddPrefill, setShowAddPrefill] = useState(false);
const [prefillInit, setPrefillInit] = useState({ id: undefined });
const [showConflict, setShowConflict] = useState(false);
const [conflicts, setConflicts] = useState([]);
const handleSyncUpstream = async () => {
// 先预览
const data = await previewUpstreamDiff?.();
const conflictItems = data?.conflicts || [];
if (conflictItems.length > 0) {
setConflicts(conflictItems);
setShowConflict(true);
return;
}
// 无冲突,直接同步缺失
await syncUpstream?.();
};
// Handle delete selected models with confirmation
const handleDeleteSelectedModels = () => {
@@ -104,6 +125,38 @@ const ModelsActions = ({
{t('未配置模型')}
</Button>
<Popover
position='bottom'
trigger='hover'
content={
<div className='p-2 max-w-[360px]'>
<div className='text-[var(--semi-color-text-2)] text-sm'>
{t(
'模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:',
)}
</div>
<a
href='https://github.com/basellm/llm-metadata'
target='_blank'
rel='noreferrer'
className='text-blue-600 underline'
>
https://github.com/basellm/llm-metadata
</a>
</div>
}
>
<Button
type='secondary'
className='flex-1 md:flex-initial'
size='small'
loading={syncing || previewing}
onClick={handleSyncUpstream}
>
{t('同步官方')}
</Button>
</Popover>
<Button
type='secondary'
className='flex-1 md:flex-initial'
@@ -165,6 +218,17 @@ const ModelsActions = ({
editingGroup={prefillInit}
onSuccess={() => setShowAddPrefill(false)}
/>
<UpstreamConflictModal
visible={showConflict}
onClose={() => setShowConflict(false)}
conflicts={conflicts}
onSubmit={async (payload) => {
return await applyUpstreamOverwrite?.(payload);
}}
t={t}
loading={syncing}
/>
</>
);
};