feat: add batch AI reorganization support to Transfer History view

This commit is contained in:
jxxghp
2026-04-29 20:37:52 +08:00
parent 96684a8d13
commit 723eb319e1
4 changed files with 105 additions and 31 deletions
+9 -5
View File
@@ -460,7 +460,8 @@ export default {
botSecret: 'Bot Secret', botSecret: 'Bot Secret',
botSecretHint: 'WebSocket secret of the WeChat Work AI bot', botSecretHint: 'WebSocket secret of the WeChat Work AI bot',
botChatId: 'Default Target', botChatId: 'Default Target',
botChatIdHint: 'Use user userid; for proactive group messages use group:chatid. Leave empty to notify known interacted users', botChatIdHint:
'Use user userid; for proactive group messages use group:chatid. Leave empty to notify known interacted users',
botChatIdPlaceholder: 'userid or group:chatid', botChatIdPlaceholder: 'userid or group:chatid',
botWsUrl: 'WebSocket URL', botWsUrl: 'WebSocket URL',
botWsUrlHint: 'WebSocket endpoint for the WeChat Work AI bot, usually the default value', botWsUrlHint: 'WebSocket endpoint for the WeChat Work AI bot, usually the default value',
@@ -1475,8 +1476,9 @@ export default {
fanartEnableHint: 'Use image data from fanart.tv', fanartEnableHint: 'Use image data from fanart.tv',
fanartLang: 'Fanart Language', fanartLang: 'Fanart Language',
fanartLangHint: 'Set language preference for Fanart images, ordered by priority when multiple selected', fanartLangHint: 'Set language preference for Fanart images, ordered by priority when multiple selected',
recognizePluginFirst: "Prioritize Plugin Recognition", recognizePluginFirst: 'Prioritize Plugin Recognition',
recognizePluginFirstHint: "Prioritize calling plugins for media recognition. If a plugin matches, native recognition will be skipped", recognizePluginFirstHint:
'Prioritize calling plugins for media recognition. If a plugin matches, native recognition will be skipped',
githubProxy: 'Github Acceleration Proxy', githubProxy: 'Github Acceleration Proxy',
githubProxyPlaceholder: 'Leave empty for no proxy', githubProxyPlaceholder: 'Leave empty for no proxy',
githubProxyHint: 'Use proxy to accelerate Github access speed', githubProxyHint: 'Use proxy to accelerate Github access speed',
@@ -1598,7 +1600,7 @@ export default {
skipDesc: 'Skip scraping, this file will not be generated', skipDesc: 'Skip scraping, this file will not be generated',
missingOnlyDesc: 'Scrape only if missing, existing file remains unchanged', missingOnlyDesc: 'Scrape only if missing, existing file remains unchanged',
overwriteDesc: 'Always scrape, existing file will be overwritten', overwriteDesc: 'Always scrape, existing file will be overwritten',
} },
}, },
site: { site: {
siteSync: 'Site Synchronization', siteSync: 'Site Synchronization',
@@ -2842,6 +2844,7 @@ export default {
actions: { actions: {
aiRedo: 'Assistant Organize', aiRedo: 'Assistant Organize',
aiRedoPending: 'Assistant Organizing...', aiRedoPending: 'Assistant Organizing...',
batchAiRedo: 'Assistant Batch Organize',
redo: 'Reorganize', redo: 'Reorganize',
delete: 'Delete', delete: 'Delete',
batchRedo: 'Batch Reorganize', batchRedo: 'Batch Reorganize',
@@ -3265,7 +3268,8 @@ export default {
infoDesc: infoDesc:
'Completing site authentication unlocks site capabilities and some plugin permissions. This step is optional and can also be configured later from the user menu.', 'Completing site authentication unlocks site capabilities and some plugin permissions. This step is optional and can also be configured later from the user menu.',
selectSiteHint: 'Choose a supported auth site and fill in the required credentials for that site', selectSiteHint: 'Choose a supported auth site and fill in the required credentials for that site',
submitHint: 'When you click Next, the wizard will immediately validate against the selected auth site and save the current parameters on success.', submitHint:
'When you click Next, the wizard will immediately validate against the selected auth site and save the current parameters on success.',
siteConfigNotExist: 'Authentication site configuration does not exist', siteConfigNotExist: 'Authentication site configuration does not exist',
fieldRequired: 'Please enter {name}', fieldRequired: 'Please enter {name}',
}, },
+1
View File
@@ -2799,6 +2799,7 @@ export default {
actions: { actions: {
aiRedo: '智能助手整理', aiRedo: '智能助手整理',
aiRedoPending: '智能助手整理中...', aiRedoPending: '智能助手整理中...',
batchAiRedo: '智能助手批量整理',
redo: '重新整理', redo: '重新整理',
delete: '删除', delete: '删除',
batchRedo: '批量重新整理', batchRedo: '批量重新整理',
+1
View File
@@ -2801,6 +2801,7 @@ export default {
actions: { actions: {
aiRedo: '智能助手整理', aiRedo: '智能助手整理',
aiRedoPending: '智能助手整理中...', aiRedoPending: '智能助手整理中...',
batchAiRedo: '智能助手批量整理',
redo: '重新整理', redo: '重新整理',
delete: '刪除', delete: '刪除',
batchRedo: '批量重新整理', batchRedo: '批量重新整理',
+94 -26
View File
@@ -59,7 +59,7 @@ const aiRedoProgressDialog = ref(false)
const aiRedoProgressActive = ref(false) const aiRedoProgressActive = ref(false)
const aiRedoProgressText = ref(t('transferHistory.actions.aiRedoPending')) const aiRedoProgressText = ref(t('transferHistory.actions.aiRedoPending'))
const aiRedoProgressSSE = ref<any>(null) const aiRedoProgressSSE = ref<any>(null)
const aiRedoProgressHistoryId = ref<number>() const aiRedoProgressHistoryIds = ref<number[]>([])
// IDS // IDS
const redoIds = ref<number[]>([]) const redoIds = ref<number[]>([])
@@ -374,6 +374,7 @@ async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
// //
async function removeBatch(deleteSrc: boolean, deleteDest: boolean) { async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
if (hasRunningAiRedo.value) return
// //
deleteConfirmDialog.value = false deleteConfirmDialog.value = false
// //
@@ -409,6 +410,7 @@ async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
// //
async function removeHistoryBatch() { async function removeHistoryBatch() {
if (hasRunningAiRedo.value) return
if (selected.value.length === 0) return if (selected.value.length === 0) return
// //
@@ -421,6 +423,7 @@ async function removeHistoryBatch() {
} }
// //
async function retransferBatch() { async function retransferBatch() {
if (hasRunningAiRedo.value) return
if (selected.value.length === 0) return if (selected.value.length === 0) return
// //
@@ -462,15 +465,14 @@ function stopAiRedoProgress() {
// AI // AI
async function finishAiRedo(success: boolean, errorMessage?: string) { async function finishAiRedo(success: boolean, errorMessage?: string) {
const historyId = aiRedoProgressHistoryId.value const historyIds = [...aiRedoProgressHistoryIds.value]
const historyIdSet = new Set(historyIds)
stopAiRedoProgress() stopAiRedoProgress()
aiRedoProgressDialog.value = false aiRedoProgressDialog.value = false
aiRedoProgressHistoryId.value = undefined aiRedoProgressHistoryIds.value = []
aiRedoIds.value = aiRedoIds.value.filter(id => !historyIdSet.has(id))
if (historyId !== undefined) { selected.value = selected.value.filter(item => !historyIdSet.has(item.id))
aiRedoIds.value = aiRedoIds.value.filter(id => id !== historyId)
}
await fetchData() await fetchData()
@@ -493,9 +495,14 @@ async function handleAiRedoProgressMessage(event: MessageEvent) {
// //
function startAiRedoProgress(historyId: number, progressKey: string) { function startAiRedoProgress(historyId: number, progressKey: string) {
startAiRedoProgressBatch([historyId], progressKey)
}
//
function startAiRedoProgressBatch(historyIds: number[], progressKey: string) {
stopAiRedoProgress() stopAiRedoProgress()
aiRedoProgressHistoryId.value = historyId aiRedoProgressHistoryIds.value = historyIds
aiRedoProgressDialog.value = true aiRedoProgressDialog.value = true
aiRedoProgressActive.value = true aiRedoProgressActive.value = true
aiRedoProgressText.value = t('transferHistory.actions.aiRedoPending') aiRedoProgressText.value = t('transferHistory.actions.aiRedoPending')
@@ -543,6 +550,44 @@ async function triggerAiRedo(item: TransferHistory) {
} }
} }
// AI
async function triggerBatchAiRedo() {
if (!aiAgentEnabled.value) {
$toast.error(t('transferHistory.aiRedoDisabled'))
return
}
if (hasRunningAiRedo.value) return
const historyIds = [...new Set(selected.value.map(item => item.id))]
if (historyIds.length === 0) return
aiRedoIds.value = [...new Set([...aiRedoIds.value, ...historyIds])]
let progressStarted = false
try {
const result: { [key: string]: any } = await api.post('history/transfer/ai-redo', {
history_ids: historyIds,
})
const progressKey = result.data?.progress_key
const acceptedIds = (result.data?.history_ids as number[] | undefined) ?? historyIds
if (!result.success || !progressKey) {
$toast.error(result.message || t('transferHistory.aiRedoFailed'))
return
}
startAiRedoProgressBatch(acceptedIds, progressKey)
selected.value = selected.value.filter(item => !acceptedIds.includes(item.id))
progressStarted = true
} catch (error) {
console.error(error)
$toast.error(t('transferHistory.aiRedoFailed'))
} finally {
if (!progressStarted) {
aiRedoIds.value = aiRedoIds.value.filter(id => !historyIds.includes(id))
}
}
}
// //
function getDropdownItems(item: TransferHistory) { function getDropdownItems(item: TransferHistory) {
return [ return [
@@ -645,7 +690,7 @@ const historyDynamicIcon = computed(() => (selected.value.length > 0 ? 'mdi-chev
const historyDynamicMenuItems = computed(() => { const historyDynamicMenuItems = computed(() => {
if (selected.value.length === 0) return undefined if (selected.value.length === 0) return undefined
return [ const items: Array<{ titleKey: string; icon: string; action: () => void; color?: string }> = [
{ {
titleKey: 'dialog.transferQueue.title', titleKey: 'dialog.transferQueue.title',
icon: 'mdi-timer-sand-paused', icon: 'mdi-timer-sand-paused',
@@ -653,22 +698,36 @@ const historyDynamicMenuItems = computed(() => {
transferQueueDialog.value = true transferQueueDialog.value = true
}, },
}, },
{
titleKey: 'transferHistory.actions.batchRedo',
icon: 'mdi-redo-variant',
action: () => {
retransferBatch()
},
},
{
titleKey: 'transferHistory.actions.batchDelete',
icon: 'mdi-trash-can-outline',
color: 'error',
action: () => {
removeHistoryBatch()
},
},
] ]
if (!hasRunningAiRedo.value) {
items.push(
{
titleKey: 'transferHistory.actions.batchAiRedo',
icon: 'mdi-robot-outline',
action: () => {
triggerBatchAiRedo()
},
},
{
titleKey: 'transferHistory.actions.batchRedo',
icon: 'mdi-redo-variant',
action: () => {
retransferBatch()
},
},
{
titleKey: 'transferHistory.actions.batchDelete',
icon: 'mdi-trash-can-outline',
color: 'error',
action: () => {
removeHistoryBatch()
},
},
)
}
return items
}) })
useDynamicButton({ useDynamicButton({
@@ -980,7 +1039,7 @@ onUnmounted(() => {
<Teleport to="body" v-if="!appMode && route.path === '/history'"> <Teleport to="body" v-if="!appMode && route.path === '/history'">
<div v-if="isRefreshed" class="compact-fab-stack compact-fab-stack--history"> <div v-if="isRefreshed" class="compact-fab-stack compact-fab-stack--history">
<VFab <VFab
v-if="selected.length > 0" v-if="selected.length > 0 && !hasRunningAiRedo"
icon="mdi-trash-can-outline" icon="mdi-trash-can-outline"
color="warning" color="warning"
variant="tonal" variant="tonal"
@@ -989,7 +1048,7 @@ onUnmounted(() => {
@click="removeHistoryBatch" @click="removeHistoryBatch"
/> />
<VFab <VFab
v-if="selected.length > 0" v-if="selected.length > 0 && !hasRunningAiRedo"
icon="mdi-redo-variant" icon="mdi-redo-variant"
color="success" color="success"
variant="tonal" variant="tonal"
@@ -997,6 +1056,15 @@ onUnmounted(() => {
class="compact-fab compact-fab--secondary" class="compact-fab compact-fab--secondary"
@click="retransferBatch" @click="retransferBatch"
/> />
<VFab
v-if="selected.length > 0 && !hasRunningAiRedo"
icon="mdi-robot-outline"
color="info"
variant="tonal"
appear
class="compact-fab compact-fab--secondary"
@click="triggerBatchAiRedo"
/>
<VFab <VFab
icon="mdi-timer-sand-paused" icon="mdi-timer-sand-paused"
color="primary" color="primary"