From febbd84ae7704dead27df97ae29241ec0dc8f84d Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Sun, 18 Oct 2026 12:57:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9A=AE=E8=82=A4=E7=AB=99=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8F=B0=20Wiki=20=E8=A1=A5=20MediaWiki=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增迁移中心 Tab - 自动迁移:填写 api.php 地址,可选 Bot 凭据,支持明确允许内网来源;可配置过时页面筛选规则(未更新天数、维护者统计窗口、维护者人数上限、过时标记) - XML 导入:上传 MediaWiki 导出文件建立暂存任务 - 迁移任务列表:状态标签、审核进度条、已审核/导入/排除统计、错误信息 - 任务详情:分页加载候选页面,逐条显示更新时间、维护者数量、可疑原因,可设置目标标题与是否覆盖,执行导入或排除 - 整批发布与回滚,失败或已回滚任务可删除记录 - 补充迁移相关 API 与类型定义 - 中英文各补 55 个文案 --- .../ui/skin-site/admin/AdminWikiPage.vue | 750 +++++++++++++++++- .../app-frontend/src/helpers/skin-site-api.ts | 126 +++ .../app-frontend/src/locales/en-US/index.json | 57 +- .../app-frontend/src/locales/zh-CN/index.json | 57 +- 4 files changed, 986 insertions(+), 4 deletions(-) diff --git a/apps/app-frontend/src/components/ui/skin-site/admin/AdminWikiPage.vue b/apps/app-frontend/src/components/ui/skin-site/admin/AdminWikiPage.vue index 6e1abe7..f0bd173 100644 --- a/apps/app-frontend/src/components/ui/skin-site/admin/AdminWikiPage.vue +++ b/apps/app-frontend/src/components/ui/skin-site/admin/AdminWikiPage.vue @@ -16,6 +16,16 @@ import { type SkinSiteWikiSettings, skinSiteWikiSettings, skinSiteWikiUpdateSettings, + skinSiteWikiMigrationCandidates, + skinSiteWikiMigrationDecide, + skinSiteWikiMigrationDelete, + skinSiteWikiMigrationJobs, + skinSiteWikiMigrationPublish, + skinSiteWikiMigrationRollback, + skinSiteWikiMigrationStartAutomatic, + skinSiteWikiMigrationStartXml, + type SkinSiteWikiMigrationCandidate, + type SkinSiteWikiMigrationJobInfo, } from '@/helpers/skin-site-api' const { formatMessage } = useVIntl() @@ -26,6 +36,198 @@ const messages = defineMessages({ tabPage: { id: 'app.skin-site.admin.wiki.tab-page', defaultMessage: 'Page' }, tabAudit: { id: 'app.skin-site.admin.wiki.tab-audit', defaultMessage: 'Audit log' }, tabSettings: { id: 'app.skin-site.admin.wiki.tab-settings', defaultMessage: 'Settings' }, + tabMigration: { id: 'app.skin-site.admin.wiki.tab-migration', defaultMessage: 'Migration' }, + migrationTitle: { + id: 'app.skin-site.admin.wiki.migration-title', + defaultMessage: 'MediaWiki migration', + }, + migrationHint: { + id: 'app.skin-site.admin.wiki.migration-hint', + defaultMessage: + 'Migrated content lands in a hidden staging area and is published in one batch after every page is reviewed.', + }, + autoTitle: { + id: 'app.skin-site.admin.wiki.auto-title', + defaultMessage: 'Read from MediaWiki API', + }, + apiUrl: { id: 'app.skin-site.admin.wiki.api-url', defaultMessage: 'api.php URL' }, + botUsername: { + id: 'app.skin-site.admin.wiki.bot-username', + defaultMessage: 'Bot username (optional)', + }, + botPassword: { + id: 'app.skin-site.admin.wiki.bot-password', + defaultMessage: 'Bot password (optional)', + }, + credentialsHint: { + id: 'app.skin-site.admin.wiki.credentials-hint', + defaultMessage: 'Credentials stay in memory for this job only. HTTPS is required when supplied.', + }, + allowPrivate: { + id: 'app.skin-site.admin.wiki.allow-private', + defaultMessage: 'Allow private network source', + }, + allowPrivateHint: { + id: 'app.skin-site.admin.wiki.allow-private-hint', + defaultMessage: 'Private sources may reach services inside the server network.', + }, + startAuto: { + id: 'app.skin-site.admin.wiki.start-auto', + defaultMessage: 'Create automatic migration job', + }, + xmlTitle: { + id: 'app.skin-site.admin.wiki.xml-title', + defaultMessage: 'Import MediaWiki XML', + }, + xmlFile: { id: 'app.skin-site.admin.wiki.xml-file', defaultMessage: 'Export file' }, + startXml: { + id: 'app.skin-site.admin.wiki.start-xml', + defaultMessage: 'Upload and stage', + }, + staleRules: { + id: 'app.skin-site.admin.wiki.stale-rules', + defaultMessage: 'Stale page detection rules', + }, + staleDays: { + id: 'app.skin-site.admin.wiki.stale-days', + defaultMessage: 'Days without update', + }, + maintainerWindow: { + id: 'app.skin-site.admin.wiki.maintainer-window', + defaultMessage: 'Maintainer window (days)', + }, + maxMaintainers: { + id: 'app.skin-site.admin.wiki.max-maintainers', + defaultMessage: 'Max maintainers', + }, + staleMarkers: { + id: 'app.skin-site.admin.wiki.stale-markers', + defaultMessage: 'Stale categories, templates or text', + }, + staleMarkersHint: { + id: 'app.skin-site.admin.wiki.stale-markers-hint', + defaultMessage: 'Comma separated', + }, + jobs: { id: 'app.skin-site.admin.wiki.jobs', defaultMessage: 'Migration jobs' }, + refreshJobs: { + id: 'app.skin-site.admin.wiki.refresh-jobs', + defaultMessage: 'Refresh', + }, + jobsEmpty: { + id: 'app.skin-site.admin.wiki.jobs-empty', + defaultMessage: 'No migration jobs yet.', + }, + reviewedCount: { + id: 'app.skin-site.admin.wiki.reviewed-count', + defaultMessage: 'Reviewed {reviewed} / {total}', + }, + importedExcluded: { + id: 'app.skin-site.admin.wiki.imported-excluded', + defaultMessage: 'Imported {imported} · Excluded {excluded}', + }, + viewJob: { id: 'app.skin-site.admin.wiki.view-job', defaultMessage: 'View job' }, + deleteJob: { + id: 'app.skin-site.admin.wiki.delete-job', + defaultMessage: 'Delete job record', + }, + confirmDeleteJob: { + id: 'app.skin-site.admin.wiki.confirm-delete-job', + defaultMessage: 'Delete migration job #{id}? This cannot be undone.', + }, + candidates: { + id: 'app.skin-site.admin.wiki.candidates', + defaultMessage: 'Candidate pages', + }, + candidatesEmpty: { + id: 'app.skin-site.admin.wiki.candidates-empty', + defaultMessage: 'Select a job to review its pages.', + }, + loadMore: { id: 'app.skin-site.admin.wiki.load-more', defaultMessage: 'Load more' }, + lastUpdated: { + id: 'app.skin-site.admin.wiki.last-updated', + defaultMessage: 'Updated {date}', + }, + maintainerCount: { + id: 'app.skin-site.admin.wiki.maintainer-count', + defaultMessage: '{count} maintainers', + }, + conflict: { + id: 'app.skin-site.admin.wiki.conflict', + defaultMessage: 'Title conflict', + }, + targetTitle: { + id: 'app.skin-site.admin.wiki.target-title', + defaultMessage: 'Target title', + }, + overwrite: { + id: 'app.skin-site.admin.wiki.overwrite', + defaultMessage: 'Overwrite', + }, + importAction: { id: 'app.skin-site.admin.wiki.import', defaultMessage: 'Import' }, + excludeAction: { id: 'app.skin-site.admin.wiki.exclude', defaultMessage: 'Exclude' }, + publish: { id: 'app.skin-site.admin.wiki.publish', defaultMessage: 'Publish batch' }, + rollback: { id: 'app.skin-site.admin.wiki.rollback', defaultMessage: 'Roll back' }, + confirmRollback: { + id: 'app.skin-site.admin.wiki.confirm-rollback', + defaultMessage: 'Roll back this migration batch? Published pages will be reverted.', + }, + stateImporting: { + id: 'app.skin-site.admin.wiki.state-importing', + defaultMessage: 'Importing', + }, + stateReview: { + id: 'app.skin-site.admin.wiki.state-review', + defaultMessage: 'Waiting review', + }, + statePublishing: { + id: 'app.skin-site.admin.wiki.state-publishing', + defaultMessage: 'Publishing', + }, + statePublished: { + id: 'app.skin-site.admin.wiki.state-published', + defaultMessage: 'Published', + }, + stateFailed: { id: 'app.skin-site.admin.wiki.state-failed', defaultMessage: 'Failed' }, + stateRolledBack: { + id: 'app.skin-site.admin.wiki.state-rolled-back', + defaultMessage: 'Rolled back', + }, + decisionPending: { + id: 'app.skin-site.admin.wiki.decision-pending', + defaultMessage: 'Pending', + }, + decisionImport: { + id: 'app.skin-site.admin.wiki.decision-import', + defaultMessage: 'Import', + }, + decisionExclude: { + id: 'app.skin-site.admin.wiki.decision-exclude', + defaultMessage: 'Exclude', + }, + migrationStarted: { + id: 'app.skin-site.admin.wiki.migration-started', + defaultMessage: 'Migration job created.', + }, + xmlStaged: { + id: 'app.skin-site.admin.wiki.xml-staged', + defaultMessage: 'XML parsed and staged for review.', + }, + published: { + id: 'app.skin-site.admin.wiki.published', + defaultMessage: 'Published {imported} pages, overwrote {overwritten}.', + }, + rolledBack: { + id: 'app.skin-site.admin.wiki.rolled-back', + defaultMessage: 'Migration batch rolled back.', + }, + jobDeleted: { + id: 'app.skin-site.admin.wiki.job-deleted', + defaultMessage: 'Migration record deleted.', + }, + selectXml: { + id: 'app.skin-site.admin.wiki.select-xml', + defaultMessage: 'Choose a MediaWiki XML file first.', + }, loading: { id: 'app.skin-site.admin.wiki.loading', defaultMessage: 'Loading…' }, pagePath: { id: 'app.skin-site.admin.wiki.page-path', defaultMessage: 'Page path' }, loadPage: { id: 'app.skin-site.admin.wiki.load-page', defaultMessage: 'Load page' }, @@ -77,7 +279,7 @@ const messages = defineMessages({ }, }) -type Tab = 'page' | 'audit' | 'settings' +type Tab = 'page' | 'audit' | 'settings' | 'migration' const tab = ref('page') const pagePath = ref('') @@ -244,6 +446,269 @@ async function switchTab(next: Tab) { tab.value = next if (next === 'audit' && !auditItems.value.length) await loadAudit(true) if (next === 'settings' && !settings.value) await loadSettings() + if (next === 'migration' && !migrationJobs.value.length) await loadMigrationJobs() +} + +// ---------- migration ---------- +const migrationJobs = ref([]) +const jobsLoading = ref(false) +const startingMigration = ref(false) + +const automaticForm = ref({ + apiUrl: '', + username: '', + password: '', + allowPrivateAddress: false, + rules: { + staleDays: 365, + maintainerWindowDays: 180, + maximumMaintainers: 1, + }, +}) +const staleMarkersText = ref('') +const xmlInput = ref(null) + +const selectedJob = ref(null) +const candidates = ref([]) +const candidatesLoading = ref(false) +const candidateNextCursor = ref(null) +const candidateTargetTitles = reactive>({}) +const candidateOverwrite = reactive>({}) + +const publishing = ref(false) +const rollingBack = ref(false) +const confirmingRollbackId = ref(null) +const deletingJob = ref(false) +const confirmingDeleteId = ref(null) + +const JOB_STATE_LABELS: Record = { + IMPORTING: '正在读取', + REVIEW: '等待审核', + PUBLISHING: '正在发布', + PUBLISHED: '已发布', + FAILED: '失败', + ROLLED_BACK: '已回滚', +} + +function jobStateLabel(state: string): string { + return JOB_STATE_LABELS[state] ?? state +} + +function jobStateClass(state: string): string { + switch (state) { + case 'IMPORTING': + case 'PUBLISHING': + return 'bg-brand text-contrast' + case 'REVIEW': + return 'bg-yellow text-contrast' + case 'PUBLISHED': + return 'bg-green text-contrast' + case 'FAILED': + return 'bg-red text-contrast' + default: + return 'bg-button-bg text-secondary' + } +} + +function decisionLabel(decision: string): string { + if (decision === 'IMPORT') return formatMessage(messages.decisionImport) + if (decision === 'EXCLUDE') return formatMessage(messages.decisionExclude) + return formatMessage(messages.decisionPending) +} + +function decisionClass(decision: string): string { + if (decision === 'IMPORT') return 'bg-green text-contrast' + if (decision === 'EXCLUDE') return 'bg-red text-contrast' + return 'bg-yellow text-contrast' +} + +async function loadMigrationJobs() { + jobsLoading.value = true + try { + const res = await skinSiteWikiMigrationJobs() + if (!res.success) throw new Error(res.errorMessage) + migrationJobs.value = res.payload ?? [] + if (selectedJob.value) { + selectedJob.value = + migrationJobs.value.find((j) => j.id === selectedJob.value!.id) ?? null + } + } catch (error) { + handleError(error) + } finally { + jobsLoading.value = false + } +} + +async function startAutomaticMigration() { + if (startingMigration.value) return + startingMigration.value = true + try { + const res = await skinSiteWikiMigrationStartAutomatic({ + apiUrl: automaticForm.value.apiUrl.trim(), + username: automaticForm.value.username.trim() || null, + password: automaticForm.value.password || null, + allowPrivateAddress: automaticForm.value.allowPrivateAddress, + rules: { + ...automaticForm.value.rules, + staleMarkers: staleMarkersText.value + .split(/[,,]/) + .map((s) => s.trim()) + .filter(Boolean), + }, + }) + if (!res.success) throw new Error(res.errorMessage) + addNotification({ type: 'success', title: formatMessage(messages.migrationStarted) }) + await loadMigrationJobs() + } catch (error) { + handleError(error) + } finally { + startingMigration.value = false + } +} + +async function startXmlMigration() { + const file = xmlInput.value?.files?.[0] + if (!file) { + addNotification({ type: 'error', title: formatMessage(messages.selectXml) }) + return + } + if (startingMigration.value) return + startingMigration.value = true + try { + const res = await skinSiteWikiMigrationStartXml(file) + if (!res.success) throw new Error(res.errorMessage) + addNotification({ type: 'success', title: formatMessage(messages.xmlStaged) }) + if (xmlInput.value) xmlInput.value.value = '' + await loadMigrationJobs() + await selectJob(res.payload) + } catch (error) { + handleError(error) + } finally { + startingMigration.value = false + } +} + +async function selectJob(job: SkinSiteWikiMigrationJobInfo) { + confirmingDeleteId.value = null + confirmingRollbackId.value = null + selectedJob.value = job + candidatesLoading.value = true + try { + const res = await skinSiteWikiMigrationCandidates(job.id) + if (selectedJob.value?.id !== job.id) return + if (!res.success) throw new Error(res.errorMessage) + candidates.value = res.payload.items ?? [] + candidateNextCursor.value = res.payload.nextCursor ?? null + applyCandidateDefaults(candidates.value) + } catch (error) { + handleError(error) + } finally { + if (selectedJob.value?.id === job.id) candidatesLoading.value = false + } +} + +function applyCandidateDefaults(items: SkinSiteWikiMigrationCandidate[]) { + for (const c of items) { + if (candidateTargetTitles[c.id] === undefined) candidateTargetTitles[c.id] = c.title + if (candidateOverwrite[c.id] === undefined) candidateOverwrite[c.id] = false + } +} + +async function loadMoreCandidates() { + if (!selectedJob.value || !candidateNextCursor.value || candidatesLoading.value) return + const jobId = selectedJob.value.id + candidatesLoading.value = true + try { + const res = await skinSiteWikiMigrationCandidates(jobId, candidateNextCursor.value) + if (selectedJob.value?.id !== jobId) return + if (!res.success) throw new Error(res.errorMessage) + candidates.value.push(...(res.payload.items ?? [])) + candidateNextCursor.value = res.payload.nextCursor ?? null + applyCandidateDefaults(res.payload.items ?? []) + } catch (error) { + handleError(error) + } finally { + if (selectedJob.value?.id === jobId) candidatesLoading.value = false + } +} + +async function decideCandidate( + candidate: SkinSiteWikiMigrationCandidate, + decision: 'IMPORT' | 'EXCLUDE', +) { + try { + const res = await skinSiteWikiMigrationDecide(candidate.id, { + decision, + targetTitle: + decision === 'IMPORT' ? candidateTargetTitles[candidate.id] || candidate.title : null, + overwrite: decision === 'IMPORT' ? Boolean(candidateOverwrite[candidate.id]) : false, + }) + if (!res.success) throw new Error(res.errorMessage) + candidate.decision = decision + await loadMigrationJobs() + } catch (error) { + handleError(error) + } +} + +async function publishMigration() { + if (!selectedJob.value || publishing.value) return + publishing.value = true + try { + const res = await skinSiteWikiMigrationPublish(selectedJob.value.id) + if (!res.success) throw new Error(res.errorMessage) + addNotification({ + type: 'success', + title: formatMessage(messages.published, { + imported: res.payload.imported, + overwritten: res.payload.overwritten, + }), + }) + await loadMigrationJobs() + } catch (error) { + handleError(error) + } finally { + publishing.value = false + } +} + +async function rollbackMigration() { + if (!selectedJob.value || rollingBack.value) return + rollingBack.value = true + try { + const res = await skinSiteWikiMigrationRollback(selectedJob.value.id) + if (!res.success) throw new Error(res.errorMessage) + confirmingRollbackId.value = null + addNotification({ type: 'success', title: formatMessage(messages.rolledBack) }) + await loadMigrationJobs() + } catch (error) { + handleError(error) + } finally { + rollingBack.value = false + } +} + +async function deleteJob(job: SkinSiteWikiMigrationJobInfo) { + if (deletingJob.value || confirmingDeleteId.value !== job.id) return + if (job.state !== 'FAILED' && job.state !== 'ROLLED_BACK') return + deletingJob.value = true + try { + const res = await skinSiteWikiMigrationDelete(job.id) + if (!res.success) throw new Error(res.errorMessage) + migrationJobs.value = migrationJobs.value.filter((j) => j.id !== job.id) + if (selectedJob.value?.id === job.id) { + selectedJob.value = null + candidates.value = [] + candidateNextCursor.value = null + } + confirmingDeleteId.value = null + addNotification({ type: 'success', title: formatMessage(messages.jobDeleted) }) + await loadMigrationJobs() + } catch (error) { + handleError(error) + } finally { + deletingJob.value = false + } } onMounted(() => void loadSettings()) @@ -276,6 +741,14 @@ onMounted(() => void loadSettings()) > {{ formatMessage(messages.tabSettings) }} + - + diff --git a/apps/app-frontend/src/helpers/skin-site-api.ts b/apps/app-frontend/src/helpers/skin-site-api.ts index 9fb7d0d..c2cf8d8 100644 --- a/apps/app-frontend/src/helpers/skin-site-api.ts +++ b/apps/app-frontend/src/helpers/skin-site-api.ts @@ -2367,3 +2367,129 @@ export function skinSiteChatVoiceCapacityUpdate( { method: 'PUT', body: data }, ) } + +// --------------------------------------------------------------------------- +// Wiki migration +// --------------------------------------------------------------------------- + +export type SkinSiteWikiMigrationState = + | 'IMPORTING' + | 'REVIEW' + | 'PUBLISHING' + | 'PUBLISHED' + | 'FAILED' + | 'ROLLED_BACK' + +export type SkinSiteWikiMigrationDecision = 'PENDING' | 'IMPORT' | 'EXCLUDE' + +export interface SkinSiteWikiMigrationJobInfo { + id: number + source: string + state: SkinSiteWikiMigrationState + total: number + reviewed: number + imported: number + excluded: number + error?: string | null + missingAttachmentCount?: number +} + +export interface SkinSiteWikiMigrationCandidate { + id: number + title: string + lastUpdatedAt: number + maintainerCount: number + suspiciousReasons: string[] + decision: SkinSiteWikiMigrationDecision + conflict: boolean +} + +export interface SkinSiteWikiMigrationPage { + items: SkinSiteWikiMigrationCandidate[] + nextCursor?: number | null +} + +export interface SkinSiteWikiMigrationPublishResult { + imported: number + overwritten: number +} + +export interface SkinSiteWikiAutomaticMigrationInput { + apiUrl: string + username?: string | null + password?: string | null + allowPrivateAddress: boolean + rules: { + staleDays: number + maintainerWindowDays: number + maximumMaintainers: number + staleMarkers: string[] + } +} + +export function skinSiteWikiMigrationJobs(): Promise< + SkinSiteResponse +> { + return skinSiteRequest('wiki/admin/migration') +} + +export function skinSiteWikiMigrationStartAutomatic( + data: SkinSiteWikiAutomaticMigrationInput, +): Promise> { + return skinSiteRequest('wiki/admin/migration/automatic', { + method: 'POST', + body: data, + }) +} + +export function skinSiteWikiMigrationStartXml( + file: File, +): Promise> { + const form = new FormData() + form.append('xml', file, file.name) + return skinSiteRequest('wiki/admin/migration/xml', { + method: 'POST', + form, + }) +} + +export function skinSiteWikiMigrationCandidates( + jobId: number, + after?: number, +): Promise> { + const suffix = after ? '?after=' + after : '' + return skinSiteRequest( + 'wiki/admin/migration/' + jobId + '/candidates' + suffix, + ) +} + +export function skinSiteWikiMigrationDecide( + candidateId: number, + data: { decision: SkinSiteWikiMigrationDecision; targetTitle?: string | null; overwrite?: boolean }, +): Promise> { + return skinSiteRequest('wiki/admin/migration/candidates/' + candidateId, { + method: 'POST', + body: data, + }) +} + +export function skinSiteWikiMigrationPublish( + jobId: number, +): Promise> { + return skinSiteRequest( + 'wiki/admin/migration/' + jobId + '/publish', + { method: 'POST' }, + ) +} + +export function skinSiteWikiMigrationRollback( + jobId: number, +): Promise> { + return skinSiteRequest('wiki/admin/migration/' + jobId + '/rollback', { + method: 'POST', + }) +} + +export function skinSiteWikiMigrationDelete(jobId: number): Promise> { + return skinSiteRequest('wiki/admin/migration/' + jobId, { method: 'DELETE' }) +} diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 3ace0fa..571bc67 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -12369,5 +12369,60 @@ "app.skin-site.chat.admin.voice-saved": {"message": "Voice configuration saved."}, "app.skin-site.chat.admin.voice-logs-auto-refresh": {"message": "Auto refresh"}, "app.skin-site.chat.admin.voice-logs-refresh": {"message": "Refresh"}, - "app.skin-site.chat.admin.voice-logs-empty": {"message": "No logs yet."} + "app.skin-site.chat.admin.voice-logs-empty": {"message": "No logs yet."}, + "app.skin-site.admin.wiki.tab-migration": {"message": "Migration"}, + "app.skin-site.admin.wiki.migration-title": {"message": "MediaWiki migration"}, + "app.skin-site.admin.wiki.migration-hint": {"message": "Migrated content lands in a hidden staging area and is published in one batch after every page is reviewed."}, + "app.skin-site.admin.wiki.auto-title": {"message": "Read from MediaWiki API"}, + "app.skin-site.admin.wiki.api-url": {"message": "api.php URL"}, + "app.skin-site.admin.wiki.bot-username": {"message": "Bot username (optional)"}, + "app.skin-site.admin.wiki.bot-password": {"message": "Bot password (optional)"}, + "app.skin-site.admin.wiki.credentials-hint": {"message": "Credentials stay in memory for this job only. HTTPS is required when supplied."}, + "app.skin-site.admin.wiki.allow-private": {"message": "Allow private network source"}, + "app.skin-site.admin.wiki.allow-private-hint": {"message": "Private sources may reach services inside the server network."}, + "app.skin-site.admin.wiki.start-auto": {"message": "Create automatic migration job"}, + "app.skin-site.admin.wiki.xml-title": {"message": "Import MediaWiki XML"}, + "app.skin-site.admin.wiki.xml-file": {"message": "Export file"}, + "app.skin-site.admin.wiki.start-xml": {"message": "Upload and stage"}, + "app.skin-site.admin.wiki.stale-rules": {"message": "Stale page detection rules"}, + "app.skin-site.admin.wiki.stale-days": {"message": "Days without update"}, + "app.skin-site.admin.wiki.maintainer-window": {"message": "Maintainer window (days)"}, + "app.skin-site.admin.wiki.max-maintainers": {"message": "Max maintainers"}, + "app.skin-site.admin.wiki.stale-markers": {"message": "Stale categories, templates or text"}, + "app.skin-site.admin.wiki.stale-markers-hint": {"message": "Comma separated"}, + "app.skin-site.admin.wiki.jobs": {"message": "Migration jobs"}, + "app.skin-site.admin.wiki.refresh-jobs": {"message": "Refresh"}, + "app.skin-site.admin.wiki.jobs-empty": {"message": "No migration jobs yet."}, + "app.skin-site.admin.wiki.reviewed-count": {"message": "Reviewed {reviewed} / {total}"}, + "app.skin-site.admin.wiki.imported-excluded": {"message": "Imported {imported} · Excluded {excluded}"}, + "app.skin-site.admin.wiki.view-job": {"message": "View job"}, + "app.skin-site.admin.wiki.delete-job": {"message": "Delete job record"}, + "app.skin-site.admin.wiki.confirm-delete-job": {"message": "Delete migration job #{id}? This cannot be undone."}, + "app.skin-site.admin.wiki.candidates": {"message": "Candidate pages"}, + "app.skin-site.admin.wiki.candidates-empty": {"message": "Select a job to review its pages."}, + "app.skin-site.admin.wiki.last-updated": {"message": "Updated {date}"}, + "app.skin-site.admin.wiki.maintainer-count": {"message": "{count} maintainers"}, + "app.skin-site.admin.wiki.conflict": {"message": "Title conflict"}, + "app.skin-site.admin.wiki.target-title": {"message": "Target title"}, + "app.skin-site.admin.wiki.overwrite": {"message": "Overwrite"}, + "app.skin-site.admin.wiki.import": {"message": "Import"}, + "app.skin-site.admin.wiki.exclude": {"message": "Exclude"}, + "app.skin-site.admin.wiki.publish": {"message": "Publish batch"}, + "app.skin-site.admin.wiki.rollback": {"message": "Roll back"}, + "app.skin-site.admin.wiki.confirm-rollback": {"message": "Roll back this migration batch? Published pages will be reverted."}, + "app.skin-site.admin.wiki.state-importing": {"message": "Importing"}, + "app.skin-site.admin.wiki.state-review": {"message": "Waiting review"}, + "app.skin-site.admin.wiki.state-publishing": {"message": "Publishing"}, + "app.skin-site.admin.wiki.state-published": {"message": "Published"}, + "app.skin-site.admin.wiki.state-failed": {"message": "Failed"}, + "app.skin-site.admin.wiki.state-rolled-back": {"message": "Rolled back"}, + "app.skin-site.admin.wiki.decision-pending": {"message": "Pending"}, + "app.skin-site.admin.wiki.decision-import": {"message": "Import"}, + "app.skin-site.admin.wiki.decision-exclude": {"message": "Exclude"}, + "app.skin-site.admin.wiki.migration-started": {"message": "Migration job created."}, + "app.skin-site.admin.wiki.xml-staged": {"message": "XML parsed and staged for review."}, + "app.skin-site.admin.wiki.published": {"message": "Published {imported} pages, overwrote {overwritten}."}, + "app.skin-site.admin.wiki.rolled-back": {"message": "Migration batch rolled back."}, + "app.skin-site.admin.wiki.job-deleted": {"message": "Migration record deleted."}, + "app.skin-site.admin.wiki.select-xml": {"message": "Choose a MediaWiki XML file first."} } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 202bffb..be7e653 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -12200,5 +12200,60 @@ "app.skin-site.chat.admin.voice-saved": {"message": "语音库配置已保存。"}, "app.skin-site.chat.admin.voice-logs-auto-refresh": {"message": "自动刷新"}, "app.skin-site.chat.admin.voice-logs-refresh": {"message": "刷新"}, - "app.skin-site.chat.admin.voice-logs-empty": {"message": "暂无日志。"} + "app.skin-site.chat.admin.voice-logs-empty": {"message": "暂无日志。"}, + "app.skin-site.admin.wiki.tab-migration": {"message": "迁移中心"}, + "app.skin-site.admin.wiki.migration-title": {"message": "MediaWiki 迁移"}, + "app.skin-site.admin.wiki.migration-hint": {"message": "迁移内容会先进入不可见暂存区,所有页面完成导入或排除审核后才能整批发布。"}, + "app.skin-site.admin.wiki.auto-title": {"message": "从 MediaWiki API 自动读取"}, + "app.skin-site.admin.wiki.api-url": {"message": "api.php 地址"}, + "app.skin-site.admin.wiki.bot-username": {"message": "Bot 用户名(选填)"}, + "app.skin-site.admin.wiki.bot-password": {"message": "Bot 密码(选填)"}, + "app.skin-site.admin.wiki.credentials-hint": {"message": "凭据只存在于本次后台任务的内存中,携带凭据时必须使用 HTTPS。"}, + "app.skin-site.admin.wiki.allow-private": {"message": "明确允许访问内网来源"}, + "app.skin-site.admin.wiki.allow-private-hint": {"message": "内网来源可能访问皮肤站所在网络中的服务,请只对可信地址启用。"}, + "app.skin-site.admin.wiki.start-auto": {"message": "创建自动迁移任务"}, + "app.skin-site.admin.wiki.xml-title": {"message": "导入 MediaWiki XML"}, + "app.skin-site.admin.wiki.xml-file": {"message": "导出文件"}, + "app.skin-site.admin.wiki.start-xml": {"message": "上传并建立暂存任务"}, + "app.skin-site.admin.wiki.stale-rules": {"message": "疑似过时页面筛选规则"}, + "app.skin-site.admin.wiki.stale-days": {"message": "未更新天数"}, + "app.skin-site.admin.wiki.maintainer-window": {"message": "维护者统计天数"}, + "app.skin-site.admin.wiki.max-maintainers": {"message": "维护者人数不超过"}, + "app.skin-site.admin.wiki.stale-markers": {"message": "过时分类、模板或文字标记"}, + "app.skin-site.admin.wiki.stale-markers-hint": {"message": "使用逗号分隔"}, + "app.skin-site.admin.wiki.jobs": {"message": "迁移任务"}, + "app.skin-site.admin.wiki.refresh-jobs": {"message": "刷新"}, + "app.skin-site.admin.wiki.jobs-empty": {"message": "还没有迁移任务。"}, + "app.skin-site.admin.wiki.reviewed-count": {"message": "已审核 {reviewed} / {total}"}, + "app.skin-site.admin.wiki.imported-excluded": {"message": "导入 {imported} · 排除 {excluded}"}, + "app.skin-site.admin.wiki.view-job": {"message": "查看任务"}, + "app.skin-site.admin.wiki.delete-job": {"message": "删除迁移记录"}, + "app.skin-site.admin.wiki.confirm-delete-job": {"message": "确认删除迁移任务 #{id}?此操作不可恢复。"}, + "app.skin-site.admin.wiki.candidates": {"message": "候选页面"}, + "app.skin-site.admin.wiki.candidates-empty": {"message": "选择任务后查看其页面。"}, + "app.skin-site.admin.wiki.last-updated": {"message": "更新于 {date}"}, + "app.skin-site.admin.wiki.maintainer-count": {"message": "{count} 位维护者"}, + "app.skin-site.admin.wiki.conflict": {"message": "标题冲突"}, + "app.skin-site.admin.wiki.target-title": {"message": "目标标题"}, + "app.skin-site.admin.wiki.overwrite": {"message": "覆盖"}, + "app.skin-site.admin.wiki.import": {"message": "导入"}, + "app.skin-site.admin.wiki.exclude": {"message": "排除"}, + "app.skin-site.admin.wiki.publish": {"message": "整批发布"}, + "app.skin-site.admin.wiki.rollback": {"message": "回滚"}, + "app.skin-site.admin.wiki.confirm-rollback": {"message": "确认回滚本批迁移?已发布的页面将被还原。"}, + "app.skin-site.admin.wiki.state-importing": {"message": "正在读取"}, + "app.skin-site.admin.wiki.state-review": {"message": "等待审核"}, + "app.skin-site.admin.wiki.state-publishing": {"message": "正在发布"}, + "app.skin-site.admin.wiki.state-published": {"message": "已发布"}, + "app.skin-site.admin.wiki.state-failed": {"message": "失败"}, + "app.skin-site.admin.wiki.state-rolled-back": {"message": "已回滚"}, + "app.skin-site.admin.wiki.decision-pending": {"message": "待审核"}, + "app.skin-site.admin.wiki.decision-import": {"message": "导入"}, + "app.skin-site.admin.wiki.decision-exclude": {"message": "排除"}, + "app.skin-site.admin.wiki.migration-started": {"message": "迁移任务已创建。"}, + "app.skin-site.admin.wiki.xml-staged": {"message": "XML 已解析并进入逐页审核阶段。"}, + "app.skin-site.admin.wiki.published": {"message": "已发布 {imported} 个页面,覆盖 {overwritten} 个。"}, + "app.skin-site.admin.wiki.rolled-back": {"message": "迁移批次已安全回滚。"}, + "app.skin-site.admin.wiki.job-deleted": {"message": "迁移记录已删除。"}, + "app.skin-site.admin.wiki.select-xml": {"message": "请先选择 MediaWiki XML 文件。"} }