feat(sync): multi-language sync wizard, backend locale support, and conflict modal UX improvements

Frontend (web)
- ModelsActions.jsx
  - Replace “Sync Official” with “Sync” and open a new two-step SyncWizard.
  - Pass selected locale through to preview, sync, and overwrite flows.
  - Keep conflict resolution flow; inject locale into overwrite submission.

- New: models/modals/SyncWizardModal.jsx
  - Two-step wizard: (1) method selection (config-sync disabled for now), (2) language selection (en/zh/ja).
  - Horizontal, centered Radio cards; returns { option, locale } via onConfirm.

- UpstreamConflictModal.jsx
  - Add search input (model fuzzy search) and native pagination.
  - Column header checkbox now only applies to rows in the current filtered result.
  - Fix “Cannot access ‘filteredDataSource’ before initialization”.
  - Refactor with useMemo/useCallback; extract helpers to remove duplicated logic:
    - getPresentRowsForField, getHeaderState, applyHeaderChange
  - Minor code cleanups and stability improvements.

- i18n (en.json)
  - Add strings for the sync wizard and related actions (Sync, Sync Wizard, Select method/source/language, etc.).
  - Adjust minor translations.

Hooks
- useModelsData.jsx
  - Extend previewUpstreamDiff, syncUpstream, applyUpstreamOverwrite to accept options with locale.
  - Send locale via query/body accordingly.

Backend (Go)
- controller/model_sync.go
  - Accept locale from query/body and resolve i18n upstream URLs.
  - Add SYNC_UPSTREAM_BASE for upstream base override (default: https://basellm.github.io/llm-metadata).
  - Make HTTP timeouts/retries/limits configurable:
    - SYNC_HTTP_TIMEOUT_SECONDS, SYNC_HTTP_RETRY, SYNC_HTTP_MAX_MB
  - Add ETag-based caching and support both envelope and pure array JSON formats.
  - Concurrently fetch vendors and models; improve error responses with locale and source URLs.
  - Include source meta (locale, models_url, vendors_url) in success payloads.

Notes
- No breaking changes expected.
- Lint passes for touched files.
This commit is contained in:
t0ng7u
2025-09-02 18:49:37 +08:00
parent 42e5794d00
commit 14af08750f
9 changed files with 856 additions and 462 deletions
@@ -21,11 +21,12 @@ import React, { useState } from 'react';
import MissingModelsModal from './modals/MissingModelsModal';
import PrefillGroupManagement from './modals/PrefillGroupManagement';
import EditPrefillGroupModal from './modals/EditPrefillGroupModal';
import { Button, Modal, Popover } from '@douyinfe/semi-ui';
import { Button, Modal, Popover, RadioGroup, Radio } 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';
import SyncWizardModal from './modals/SyncWizardModal';
const ModelsActions = ({
selectedKeys,
@@ -50,10 +51,12 @@ const ModelsActions = ({
const [prefillInit, setPrefillInit] = useState({ id: undefined });
const [showConflict, setShowConflict] = useState(false);
const [conflicts, setConflicts] = useState([]);
const [showSyncModal, setShowSyncModal] = useState(false);
const [syncLocale, setSyncLocale] = useState('zh');
const handleSyncUpstream = async () => {
const handleSyncUpstream = async (locale) => {
// 先预览
const data = await previewUpstreamDiff?.();
const data = await previewUpstreamDiff?.({ locale });
const conflictItems = data?.conflicts || [];
if (conflictItems.length > 0) {
setConflicts(conflictItems);
@@ -61,7 +64,7 @@ const ModelsActions = ({
return;
}
// 无冲突,直接同步缺失
await syncUpstream?.();
await syncUpstream?.({ locale });
};
// Handle delete selected models with confirmation
@@ -151,9 +154,12 @@ const ModelsActions = ({
className='flex-1 md:flex-initial'
size='small'
loading={syncing || previewing}
onClick={handleSyncUpstream}
onClick={() => {
setSyncLocale('zh');
setShowSyncModal(true);
}}
>
{t('同步官方')}
{t('同步')}
</Button>
</Popover>
@@ -196,6 +202,20 @@ const ModelsActions = ({
</div>
</Modal>
<SyncWizardModal
visible={showSyncModal}
onClose={() => setShowSyncModal(false)}
loading={syncing || previewing}
t={t}
onConfirm={async ({ option, locale }) => {
setSyncLocale(locale);
if (option === 'official') {
await handleSyncUpstream(locale);
}
setShowSyncModal(false);
}}
/>
<MissingModelsModal
visible={showMissingModal}
onClose={() => setShowMissingModal(false)}
@@ -224,7 +244,10 @@ const ModelsActions = ({
onClose={() => setShowConflict(false)}
conflicts={conflicts}
onSubmit={async (payload) => {
return await applyUpstreamOverwrite?.(payload);
return await applyUpstreamOverwrite?.({
...payload,
locale: syncLocale,
});
}}
t={t}
loading={syncing}