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"