mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(sync): add option to delete imported chats when removing subscription
- Show confirmation dialog with two choices: keep data or delete imported chats - Backend supports deleteData param across CLI routes and Electron IPC - DataSourceManager.removeSession now returns the removed session info - Add i18n strings for the delete confirmation dialog (zh-CN/en-US/ja-JP/zh-TW)
This commit is contained in:
1 parent
31fc9be1ed
commit
7d5c541933
12 files changed
+228
-50
No files matched your search
@@ -15,6 +15,7 @@ import { registerAutomationRoutes } from './routes'
|
||||
export interface SyncRouteContext {
|
||||
dsManager: DataSourceManager
|
||||
pullEngine: PullEngine
|
||||
dbManager: DatabaseManager
|
||||
serverInfo: { port: number; host: string; token: string }
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ export function initSync(
|
||||
logger: syncLogger,
|
||||
})
|
||||
|
||||
registerAutomationRoutes(server, { dsManager, pullEngine, serverInfo })
|
||||
registerAutomationRoutes(server, { dsManager, pullEngine, dbManager, serverInfo })
|
||||
|
||||
initScheduler({
|
||||
dsManager,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import type { SyncRouteContext } from './index'
|
||||
|
||||
export function registerAutomationRoutes(server: FastifyInstance, ctx: SyncRouteContext): void {
|
||||
const { dsManager, pullEngine, serverInfo } = ctx
|
||||
const { dsManager, pullEngine, dbManager, serverInfo } = ctx
|
||||
|
||||
// ==================== Config (read-only in CLI mode) ====================
|
||||
|
||||
@@ -85,10 +85,17 @@ export function registerAutomationRoutes(server: FastifyInstance, ctx: SyncRoute
|
||||
|
||||
server.delete<{
|
||||
Params: { id: string; sessId: string }
|
||||
Querystring: { deleteData?: string }
|
||||
}>('/_web/automation/data-sources/:id/sessions/:sessId', async (request, reply) => {
|
||||
const ok = dsManager.removeSession(request.params.id, request.params.sessId)
|
||||
if (!ok) return reply.code(404).send({ error: 'Session not found' })
|
||||
const removed = dsManager.removeSession(request.params.id, request.params.sessId)
|
||||
if (!removed) return reply.code(404).send({ error: 'Session not found' })
|
||||
reloadTimer(request.params.id)
|
||||
if (request.query.deleteData === 'true' && removed.targetSessionId) {
|
||||
dbManager.close(removed.targetSessionId)
|
||||
const dbPath = dbManager.getDbPath(removed.targetSessionId)
|
||||
const fs = await import('node:fs')
|
||||
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath)
|
||||
}
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
@@ -105,6 +112,12 @@ export function registerAutomationRoutes(server: FastifyInstance, ctx: SyncRoute
|
||||
return pullEngine.triggerPullAll(request.params.id)
|
||||
})
|
||||
|
||||
// ==================== Sync Progress ====================
|
||||
|
||||
server.get('/_web/automation/sync-progress', async () => {
|
||||
return pullEngine.getProgress()
|
||||
})
|
||||
|
||||
// ==================== Remote Session Discovery ====================
|
||||
|
||||
server.get<{
|
||||
|
||||
@@ -11,6 +11,7 @@ import { setConfigManager } from '../api'
|
||||
import { getSettingsDir } from '../paths'
|
||||
import { apiLogger } from '../api/logger'
|
||||
import { getImportingStatus } from '../api/routes/import'
|
||||
import { deleteSession as deleteSessionFile } from '../database/core'
|
||||
import { ElectronFetcher, WorkerImporter, BrowserWindowNotifier } from '../api/adapters'
|
||||
import {
|
||||
ConfigManager,
|
||||
@@ -185,10 +186,14 @@ export function registerApiHandlers(_ctx: IpcContext): void {
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle('api:removeImportSession', (_event, sourceId: string, sessionId: string) => {
|
||||
const result = dsManager.removeSession(sourceId, sessionId)
|
||||
ipcMain.handle('api:removeImportSession', (_event, sourceId: string, sessionId: string, deleteData?: boolean) => {
|
||||
const removed = dsManager.removeSession(sourceId, sessionId)
|
||||
if (!removed) return false
|
||||
reloadTimer(sourceId)
|
||||
return result
|
||||
if (deleteData && removed.targetSessionId) {
|
||||
deleteSessionFile(removed.targetSessionId)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// ==================== Sync ====================
|
||||
|
||||
@@ -126,8 +126,8 @@ export const apiServerApi = {
|
||||
return ipcRenderer.invoke('api:addImportSessions', sourceId, sessions)
|
||||
},
|
||||
|
||||
removeImportSession: (sourceId: string, sessionId: string): Promise<boolean> => {
|
||||
return ipcRenderer.invoke('api:removeImportSession', sourceId, sessionId)
|
||||
removeImportSession: (sourceId: string, sessionId: string, deleteData?: boolean): Promise<boolean> => {
|
||||
return ipcRenderer.invoke('api:removeImportSession', sourceId, sessionId, deleteData)
|
||||
},
|
||||
|
||||
// ==================== 同步 ====================
|
||||
|
||||
Vendored
+1
-1
@@ -1074,7 +1074,7 @@ interface ApiServerApi {
|
||||
sourceId: string,
|
||||
sessions: Array<{ name: string; remoteSessionId: string }>
|
||||
) => Promise<ImportSession[]>
|
||||
removeImportSession: (sourceId: string, sessionId: string) => Promise<boolean>
|
||||
removeImportSession: (sourceId: string, sessionId: string, deleteData?: boolean) => Promise<boolean>
|
||||
triggerPull: (sourceId: string, sessionId?: string) => Promise<{ success: boolean; error?: string }>
|
||||
triggerPullAll: (sourceId: string) => Promise<{ success: boolean; error?: string }>
|
||||
fetchRemoteSessions: (
|
||||
|
||||
@@ -166,15 +166,15 @@ export class DataSourceManager {
|
||||
return added
|
||||
}
|
||||
|
||||
removeSession(sourceId: string, sessionId: string): boolean {
|
||||
removeSession(sourceId: string, sessionId: string): ImportSession | null {
|
||||
const sources = this.loadAll()
|
||||
const ds = sources.find((s) => s.id === sourceId)
|
||||
if (!ds) return false
|
||||
const before = ds.sessions.length
|
||||
if (!ds) return null
|
||||
const removed = ds.sessions.find((s) => s.id === sessionId)
|
||||
if (!removed) return null
|
||||
ds.sessions = ds.sessions.filter((s) => s.id !== sessionId)
|
||||
if (ds.sessions.length === before) return false
|
||||
this.saveAll(sources)
|
||||
return true
|
||||
return removed
|
||||
}
|
||||
|
||||
updateSession(sourceId: string, sessionId: string, updates: Partial<ImportSession>): ImportSession | null {
|
||||
|
||||
@@ -10,7 +10,7 @@ import McpSettingsTab from './McpSettingsTab.vue'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const store = useApiServerStore()
|
||||
const { config, status, loading, isRunning, hasError, isPortInUse, dataSources, pullingId, isWebMode } =
|
||||
const { config, status, loading, isRunning, hasError, isPortInUse, dataSources, pullingIds, syncProgress, isWebMode } =
|
||||
storeToRefs(store)
|
||||
|
||||
const activeSubTab = ref('sync')
|
||||
@@ -45,6 +45,8 @@ const showManageModal = ref(false)
|
||||
const managingDataSource = ref<DataSource | null>(null)
|
||||
const showDeleteModal = ref(false)
|
||||
const deletingDataSource = ref<DataSource | null>(null)
|
||||
const showDeleteSessionModal = ref(false)
|
||||
const deletingSession = ref<{ ds: DataSource; sess: ImportSession } | null>(null)
|
||||
|
||||
let unlistenStartupError: (() => void) | null = null
|
||||
let unlistenPullResult: (() => void) | null = null
|
||||
@@ -175,8 +177,25 @@ async function removeSource() {
|
||||
deletingDataSource.value = null
|
||||
}
|
||||
|
||||
async function removeSession(ds: DataSource, sess: ImportSession) {
|
||||
await store.removeImportSession(ds.id, sess.id)
|
||||
function confirmRemoveSession(ds: DataSource, sess: ImportSession) {
|
||||
deletingSession.value = { ds, sess }
|
||||
showDeleteSessionModal.value = true
|
||||
}
|
||||
|
||||
async function removeSessionKeepData() {
|
||||
if (!deletingSession.value) return
|
||||
const { ds, sess } = deletingSession.value
|
||||
await store.removeImportSession(ds.id, sess.id, false)
|
||||
showDeleteSessionModal.value = false
|
||||
deletingSession.value = null
|
||||
}
|
||||
|
||||
async function removeSessionDeleteData() {
|
||||
if (!deletingSession.value) return
|
||||
const { ds, sess } = deletingSession.value
|
||||
await store.removeImportSession(ds.id, sess.id, true)
|
||||
showDeleteSessionModal.value = false
|
||||
deletingSession.value = null
|
||||
}
|
||||
|
||||
async function syncAllInSource(ds: DataSource) {
|
||||
@@ -248,10 +267,22 @@ function subscribedRemoteIds(ds: DataSource): Set<string> {
|
||||
<UButton size="xs" variant="ghost" @click="toggleSourceEnabled(ds)">
|
||||
<UIcon :name="ds.enabled ? 'i-heroicons-pause' : 'i-heroicons-play'" class="h-3.5 w-3.5" />
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="syncAllInSource(ds)">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:loading="pullingIds.has(ds.id)"
|
||||
:disabled="pullingIds.has(ds.id)"
|
||||
@click="syncAllInSource(ds)"
|
||||
>
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-3.5 w-3.5" />
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" color="error" @click="confirmDeleteSource(ds)">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="error"
|
||||
:disabled="ds.sessions.some((s) => pullingIds.has(s.id))"
|
||||
@click="confirmDeleteSource(ds)"
|
||||
>
|
||||
<UIcon name="i-heroicons-trash" class="h-3.5 w-3.5" />
|
||||
</UButton>
|
||||
</div>
|
||||
@@ -269,11 +300,13 @@ function subscribedRemoteIds(ds: DataSource): Set<string> {
|
||||
<span
|
||||
class="inline-block h-2 w-2 rounded-full"
|
||||
:class="
|
||||
sess.lastStatus === 'success'
|
||||
? 'bg-green-500'
|
||||
: sess.lastStatus === 'error'
|
||||
? 'bg-red-500'
|
||||
: 'bg-gray-400'
|
||||
pullingIds.has(sess.id)
|
||||
? 'animate-pulse bg-blue-500'
|
||||
: sess.lastStatus === 'success'
|
||||
? 'bg-green-500'
|
||||
: sess.lastStatus === 'error'
|
||||
? 'bg-red-500'
|
||||
: 'bg-gray-400'
|
||||
"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ sess.name }}</span>
|
||||
@@ -282,22 +315,41 @@ function subscribedRemoteIds(ds: DataSource): Set<string> {
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:loading="pullingId === sess.id"
|
||||
:loading="pullingIds.has(sess.id)"
|
||||
:disabled="pullingIds.has(sess.id)"
|
||||
@click="syncSession(ds, sess)"
|
||||
>
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-3.5 w-3.5" />
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" color="error" @click="removeSession(ds, sess)">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="error"
|
||||
:disabled="pullingIds.has(sess.id)"
|
||||
@click="confirmRemoveSession(ds, sess)"
|
||||
>
|
||||
<UIcon name="i-heroicons-trash" class="h-3.5 w-3.5" />
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>
|
||||
{{ t('settings.api.dataSources.every') }} {{ ds.intervalMinutes }}
|
||||
{{ t('settings.api.dataSources.minutes') }}
|
||||
</span>
|
||||
<template v-if="sess.lastPullAt">
|
||||
<template v-if="syncProgress.get(sess.id)">
|
||||
<span class="text-blue-500">
|
||||
{{ t('settings.api.dataSources.syncing') }}...
|
||||
{{ syncProgress.get(sess.id)!.current }}
|
||||
{{ t('settings.api.dataSources.messages') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="pullingIds.has(sess.id) && !syncProgress.get(sess.id)">
|
||||
<span class="text-blue-500">{{ t('settings.api.dataSources.syncing') }}...</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>
|
||||
{{ t('settings.api.dataSources.every') }} {{ ds.intervalMinutes }}
|
||||
{{ t('settings.api.dataSources.minutes') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="!pullingIds.has(sess.id) && sess.lastPullAt">
|
||||
<span class="text-gray-300 dark:text-gray-600">·</span>
|
||||
<span class="text-gray-400">
|
||||
{{ t('settings.api.dataSources.lastSync') }}: {{ formatTime(sess.lastPullAt) }}
|
||||
@@ -530,7 +582,7 @@ function subscribedRemoteIds(ds: DataSource): Set<string> {
|
||||
@sessions-added="handleSessionsAdded"
|
||||
/>
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
<!-- Delete data source confirmation modal -->
|
||||
<UModal v-model:open="showDeleteModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
@@ -547,5 +599,28 @@ function subscribedRemoteIds(ds: DataSource): Set<string> {
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<!-- Delete session confirmation modal -->
|
||||
<UModal v-model:open="showDeleteSessionModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.api.dataSources.deleteSessionConfirm.title') }}
|
||||
</h3>
|
||||
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t('settings.api.dataSources.deleteSessionConfirm.message', { name: deletingSession?.sess.name }) }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton variant="soft" @click="showDeleteSessionModal = false">{{ t('common.cancel') }}</UButton>
|
||||
<UButton variant="soft" @click="removeSessionKeepData">
|
||||
{{ t('settings.api.dataSources.deleteSessionConfirm.keepData') }}
|
||||
</UButton>
|
||||
<UButton color="error" @click="removeSessionDeleteData">
|
||||
{{ t('settings.api.dataSources.deleteSessionConfirm.deleteData') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -494,6 +494,8 @@
|
||||
"every": "Every",
|
||||
"minutes": "min",
|
||||
"lastSync": "Last sync",
|
||||
"syncing": "Syncing",
|
||||
"messages": "messages",
|
||||
"addBtn": "Add Data Source",
|
||||
"form": {
|
||||
"modalTitle": "Add Data Source",
|
||||
@@ -544,6 +546,12 @@
|
||||
"deleteConfirm": {
|
||||
"title": "Delete Data Source",
|
||||
"message": "Are you sure you want to delete \"{name}\"? All import configurations under this data source will be removed. Already imported local conversations will not be deleted."
|
||||
},
|
||||
"deleteSessionConfirm": {
|
||||
"title": "Delete Subscription",
|
||||
"message": "Are you sure you want to delete \"{name}\"?",
|
||||
"keepData": "Remove subscription only",
|
||||
"deleteData": "Also delete imported chats"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
||||
@@ -493,6 +493,8 @@
|
||||
"every": "",
|
||||
"minutes": "分ごと",
|
||||
"lastSync": "最終同期",
|
||||
"syncing": "同期中",
|
||||
"messages": "件",
|
||||
"addBtn": "データソースを追加",
|
||||
"form": {
|
||||
"modalTitle": "データソースを追加",
|
||||
@@ -544,6 +546,12 @@
|
||||
"deleteConfirm": {
|
||||
"title": "データソースを削除",
|
||||
"message": "データソース「{name}」を削除してもよろしいですか?このデータソース下のすべてのインポート設定が削除されます。既にインポート済みのローカル会話は削除されません。"
|
||||
},
|
||||
"deleteSessionConfirm": {
|
||||
"title": "サブスクリプションを削除",
|
||||
"message": "「{name}」を削除してもよろしいですか?",
|
||||
"keepData": "サブスクリプションのみ削除",
|
||||
"deleteData": "インポートしたチャットも削除"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
||||
@@ -494,6 +494,8 @@
|
||||
"every": "每",
|
||||
"minutes": "分钟",
|
||||
"lastSync": "上次同步",
|
||||
"syncing": "同步中",
|
||||
"messages": "条消息",
|
||||
"addBtn": "添加数据源",
|
||||
"form": {
|
||||
"modalTitle": "添加数据源",
|
||||
@@ -544,6 +546,12 @@
|
||||
"deleteConfirm": {
|
||||
"title": "删除数据源",
|
||||
"message": "确认删除数据源「{name}」?该数据源下的所有导入配置将被移除。已导入的本地对话不会被删除。"
|
||||
},
|
||||
"deleteSessionConfirm": {
|
||||
"title": "删除订阅",
|
||||
"message": "确认删除订阅「{name}」?",
|
||||
"keepData": "仅删除订阅",
|
||||
"deleteData": "同时删除导入的聊天"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
||||
@@ -493,6 +493,8 @@
|
||||
"every": "每",
|
||||
"minutes": "分鐘",
|
||||
"lastSync": "上次同步",
|
||||
"syncing": "同步中",
|
||||
"messages": "則訊息",
|
||||
"addBtn": "新增資料來源",
|
||||
"form": {
|
||||
"modalTitle": "新增資料來源",
|
||||
@@ -544,6 +546,12 @@
|
||||
"deleteConfirm": {
|
||||
"title": "刪除資料來源",
|
||||
"message": "確認刪除資料來源「{name}」?該資料來源下的所有匯入配置將被移除。已匯入的本機對話不會被刪除。"
|
||||
},
|
||||
"deleteSessionConfirm": {
|
||||
"title": "刪除訂閱",
|
||||
"message": "確認刪除訂閱「{name}」?",
|
||||
"keepData": "僅刪除訂閱",
|
||||
"deleteData": "同時刪除匯入的聊天"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
||||
+69
-17
@@ -93,7 +93,7 @@ interface ApiTransport {
|
||||
sourceId: string,
|
||||
sessions: Array<{ name: string; remoteSessionId: string }>
|
||||
): Promise<ImportSession[]>
|
||||
removeImportSession(sourceId: string, sessionId: string): Promise<boolean>
|
||||
removeImportSession(sourceId: string, sessionId: string, deleteData?: boolean): Promise<boolean>
|
||||
triggerPull(sourceId: string, sessionId?: string): Promise<{ success: boolean; error?: string }>
|
||||
triggerPullAll(sourceId: string): Promise<{ success: boolean; error?: string }>
|
||||
onPullResult(cb: () => void): () => void
|
||||
@@ -118,7 +118,7 @@ function createElectronTransport(): ApiTransport {
|
||||
updateDataSource: (id, updates) => api.updateDataSource(id, updates),
|
||||
deleteDataSource: (id) => api.deleteDataSource(id),
|
||||
addImportSessions: (sourceId, sessions) => api.addImportSessions(sourceId, sessions),
|
||||
removeImportSession: (sourceId, sessionId) => api.removeImportSession(sourceId, sessionId),
|
||||
removeImportSession: (sourceId, sessionId, deleteData?) => api.removeImportSession(sourceId, sessionId, deleteData),
|
||||
triggerPull: (sourceId, sessionId?) => api.triggerPull(sourceId, sessionId),
|
||||
triggerPullAll: (sourceId) => api.triggerPullAll(sourceId),
|
||||
onPullResult: (cb) => api.onPullResult(cb),
|
||||
@@ -196,9 +196,10 @@ function createWebTransport(): ApiTransport {
|
||||
body: JSON.stringify({ sessions }),
|
||||
}),
|
||||
|
||||
removeImportSession: async (sourceId, sessionId) => {
|
||||
removeImportSession: async (sourceId, sessionId, deleteData?) => {
|
||||
const qs = deleteData ? '?deleteData=true' : ''
|
||||
const result = await fetchJson<{ success: boolean }>(
|
||||
`/_web/automation/data-sources/${sourceId}/sessions/${sessionId}`,
|
||||
`/_web/automation/data-sources/${sourceId}/sessions/${sessionId}${qs}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
return result.success
|
||||
@@ -254,7 +255,8 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSources = ref<DataSource[]>([])
|
||||
const pullingId = ref<string | null>(null)
|
||||
const pullingIds = ref(new Set<string>())
|
||||
const syncProgress = ref<Map<string, { current: number; pages: number }>>(new Map())
|
||||
const available = computed(() => true)
|
||||
|
||||
const isRunning = computed(() => status.value.running)
|
||||
@@ -385,7 +387,12 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
const added = await transport.addImportSessions(sourceId, sessions)
|
||||
await fetchDataSources()
|
||||
if (added.length > 0) {
|
||||
pollDataSourceUpdates(sourceId)
|
||||
for (const s of added) pullingIds.value.add(s.id)
|
||||
pullingIds.value = new Set(pullingIds.value)
|
||||
pollDataSourceUpdates(
|
||||
sourceId,
|
||||
added.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
return added
|
||||
} catch (err) {
|
||||
@@ -394,22 +401,45 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function pollDataSourceUpdates(sourceId: string, maxAttempts = 10, intervalMs = 3000) {
|
||||
function pollDataSourceUpdates(sourceId: string, sessionIds: string[], maxAttempts = 24, intervalMs = 5000) {
|
||||
let attempt = 0
|
||||
const timer = setInterval(async () => {
|
||||
attempt++
|
||||
await fetchDataSources()
|
||||
await Promise.all([fetchDataSources(), fetchSyncProgress()])
|
||||
const ds = dataSources.value.find((s) => s.id === sourceId)
|
||||
const allDone = ds?.sessions.every((s) => s.lastStatus !== 'idle')
|
||||
if (allDone || attempt >= maxAttempts) {
|
||||
const pending = sessionIds.filter((id) => {
|
||||
const sess = ds?.sessions.find((s) => s.id === id)
|
||||
return sess && sess.lastStatus === 'idle'
|
||||
})
|
||||
if (pending.length === 0 || attempt >= maxAttempts) {
|
||||
for (const id of sessionIds) {
|
||||
pullingIds.value.delete(id)
|
||||
syncProgress.value.delete(id)
|
||||
}
|
||||
pullingIds.value = new Set(pullingIds.value)
|
||||
syncProgress.value = new Map(syncProgress.value)
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
async function removeImportSession(sourceId: string, sessionId: string) {
|
||||
async function fetchSyncProgress() {
|
||||
if (!isWebMode.value) return
|
||||
try {
|
||||
const ok = await transport.removeImportSession(sourceId, sessionId)
|
||||
const list = await fetch('/_web/automation/sync-progress').then((r) => r.json())
|
||||
const map = new Map<string, { current: number; pages: number }>()
|
||||
for (const item of list as Array<{ sessionId: string; current: number; pages: number; done: boolean }>) {
|
||||
if (!item.done) map.set(item.sessionId, { current: item.current, pages: item.pages })
|
||||
}
|
||||
syncProgress.value = map
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function removeImportSession(sourceId: string, sessionId: string, deleteData?: boolean) {
|
||||
try {
|
||||
const ok = await transport.removeImportSession(sourceId, sessionId, deleteData)
|
||||
if (ok) await fetchDataSources()
|
||||
return ok
|
||||
} catch (err) {
|
||||
@@ -421,7 +451,10 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
// ==================== 同步 ====================
|
||||
|
||||
async function triggerPull(sourceId: string, sessionId?: string) {
|
||||
pullingId.value = sessionId || sourceId
|
||||
const trackId = sessionId || sourceId
|
||||
pullingIds.value.add(trackId)
|
||||
pullingIds.value = new Set(pullingIds.value)
|
||||
const progressTimer = startProgressPolling()
|
||||
try {
|
||||
const result = await transport.triggerPull(sourceId, sessionId)
|
||||
await fetchDataSources()
|
||||
@@ -430,12 +463,20 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
console.error('[ApiServerStore] Failed to trigger pull:', err)
|
||||
return { success: false, error: String(err) }
|
||||
} finally {
|
||||
pullingId.value = null
|
||||
clearInterval(progressTimer)
|
||||
pullingIds.value.delete(trackId)
|
||||
pullingIds.value = new Set(pullingIds.value)
|
||||
syncProgress.value.delete(trackId)
|
||||
syncProgress.value = new Map(syncProgress.value)
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerPullAll(sourceId: string) {
|
||||
pullingId.value = sourceId
|
||||
const ds = dataSources.value.find((s) => s.id === sourceId)
|
||||
const ids = [sourceId, ...(ds?.sessions.map((s) => s.id) ?? [])]
|
||||
for (const id of ids) pullingIds.value.add(id)
|
||||
pullingIds.value = new Set(pullingIds.value)
|
||||
const progressTimer = startProgressPolling()
|
||||
try {
|
||||
const result = await transport.triggerPullAll(sourceId)
|
||||
await fetchDataSources()
|
||||
@@ -444,10 +485,20 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
console.error('[ApiServerStore] Failed to trigger pull all:', err)
|
||||
return { success: false, error: String(err) }
|
||||
} finally {
|
||||
pullingId.value = null
|
||||
clearInterval(progressTimer)
|
||||
for (const id of ids) {
|
||||
pullingIds.value.delete(id)
|
||||
syncProgress.value.delete(id)
|
||||
}
|
||||
pullingIds.value = new Set(pullingIds.value)
|
||||
syncProgress.value = new Map(syncProgress.value)
|
||||
}
|
||||
}
|
||||
|
||||
function startProgressPolling(): ReturnType<typeof setInterval> {
|
||||
return setInterval(fetchSyncProgress, 3000)
|
||||
}
|
||||
|
||||
function listenPullResult() {
|
||||
return transport.onPullResult(() => {
|
||||
fetchDataSources()
|
||||
@@ -467,7 +518,8 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
status,
|
||||
loading,
|
||||
dataSources,
|
||||
pullingId,
|
||||
pullingIds,
|
||||
syncProgress,
|
||||
available,
|
||||
isRunning,
|
||||
hasError,
|
||||
|
||||
Reference in new issue
Block a user