fix: make inbox confirmation retry safe

This commit is contained in:
2026-07-21 18:42:47 +08:00
parent 85a25ca017
commit 7c363bb473
4 changed files with 83 additions and 25 deletions

View File

@@ -1,8 +1,8 @@
import { useState } from 'react'
import { useRef, useState } from 'react'
import { ConfigProvider, Message, Spin } from '@arco-design/web-react'
import '@arco-design/web-react/dist/css/arco.css'
import '../App.css'
import { login, setApiBaseUrl, type ApiSession } from '../api/client'
import { ApiError, login, setApiBaseUrl, type ApiSession } from '../api/client'
import { analyzeInboxItem, confirmInboxItem } from '../api/inbox'
import { mapWorkspace } from '../api/mappers'
import {
@@ -33,6 +33,7 @@ function App() {
const [session, setSession] = useState<ApiSession | null>(null)
const [workspaces, setWorkspaces] = useState<ProjectWorkspace[]>([])
const [activeProjectID, setActiveProjectID] = useState<string>('')
const activeProjectIDRef = useRef('')
const [activeChannel, setActiveChannel] = useState<ChannelKey>('overview')
const [activeTaskID, setActiveTaskID] = useState<string | null>(null)
const [, setSelectedItem] = useState('探索采集')
@@ -46,14 +47,24 @@ function App() {
const activeWorkspace = workspaces.find((workspace) => workspace.project.id === activeProjectID) ?? workspaces[0]
const activeTagOptions = activeWorkspace?.tags.filter((tag) => tag !== 'all' && tag !== '全部') ?? []
async function loadWorkspaces(nextSession: ApiSession, preferredProjectID = activeProjectID) {
function selectActiveProject(projectID: string) {
activeProjectIDRef.current = projectID
setActiveProjectID(projectID)
}
async function loadWorkspaces(nextSession: ApiSession, preferredProjectID?: string) {
const projectIDWhenStarted = activeProjectIDRef.current
const backendProjects = await fetchProjects(nextSession)
const backendWorkspaces = await Promise.all(
backendProjects.map((project, index) => fetchProjectWorkspace(nextSession, project.id).then((workspace) => mapWorkspace(workspace, index))),
)
setWorkspaces(backendWorkspaces)
const nextProjectID = backendWorkspaces.find((workspace) => workspace.project.id === preferredProjectID)?.project.id ?? backendWorkspaces[0]?.project.id ?? ''
setActiveProjectID(nextProjectID)
const latestProjectID = activeProjectIDRef.current
const requestedProjectID = latestProjectID !== projectIDWhenStarted
? latestProjectID
: preferredProjectID ?? latestProjectID
const nextProjectID = backendWorkspaces.find((workspace) => workspace.project.id === requestedProjectID)?.project.id ?? backendWorkspaces[0]?.project.id ?? ''
selectActiveProject(nextProjectID)
return backendWorkspaces
}
@@ -64,7 +75,7 @@ function App() {
const nextSession = await login(input.email, input.password)
const backendWorkspaces = await loadWorkspaces(nextSession, '')
setSession(nextSession)
setActiveProjectID(backendWorkspaces[0]?.project.id ?? '')
selectActiveProject(backendWorkspaces[0]?.project.id ?? '')
setActiveChannel('overview')
setActiveView('workspace')
setScreen('workbench')
@@ -75,7 +86,7 @@ function App() {
}
}
async function refreshAfterAction(nextProjectID = activeProjectID) {
async function refreshAfterAction(nextProjectID?: string) {
if (!session) return
await loadWorkspaces(session, nextProjectID)
}
@@ -124,7 +135,7 @@ function App() {
const nextProjectID = created.id
await refreshAfterAction(nextProjectID)
if (nextProjectID) {
setActiveProjectID(nextProjectID)
selectActiveProject(nextProjectID)
setActiveView('project')
setActiveChannel('overview')
}
@@ -198,7 +209,7 @@ function App() {
}
function openTask(project: Project, taskID: string) {
setActiveProjectID(project.id)
selectActiveProject(project.id)
setActiveView('project')
setActiveChannel('tasks')
setActiveTaskID(taskID)
@@ -236,7 +247,25 @@ function App() {
}
async function handleConfirmInbox(inboxId: string, suggestionIds: string[]) {
const response = await confirmInboxItem(requireSession(), inboxId, suggestionIds)
const confirmationProjectID = activeProjectIDRef.current
let response
try {
response = await confirmInboxItem(requireSession(), inboxId, suggestionIds)
} catch (error) {
if (!(error instanceof ApiError) || error.status !== 409) throw error
if (activeProjectIDRef.current !== confirmationProjectID) {
throw new ApiError(409, 'conflict', '确认状态发生冲突,请重新核对当前工作区')
}
try {
await refreshAfterAction()
} catch {
throw new ApiError(409, 'conflict', '确认状态发生冲突,请重新进入项目核对')
}
throw new ApiError(409, 'conflict', '确认状态发生冲突,工作区已刷新,请重新核对')
}
if (activeProjectIDRef.current !== confirmationProjectID) {
return { createdCount: response.createdCount }
}
try {
await refreshAfterAction()
return { createdCount: response.createdCount }
@@ -263,7 +292,7 @@ function App() {
}
return
}
setActiveProjectID(owningWorkspace.project.id)
selectActiveProject(owningWorkspace.project.id)
setActiveView('project')
setActiveChannel(target.channel)
setActiveTaskID(target.openTask ? result.id : null)
@@ -288,7 +317,7 @@ function App() {
onSelectWorkspace={() => setActiveView('workspace')}
onSelectWorkspaceExplore={() => setActiveView('workspace-explore')}
onSelectProject={(project) => {
setActiveProjectID(project.id)
selectActiveProject(project.id)
setActiveView('project')
setActiveChannel('overview')
setActiveTaskID(null)

View File

@@ -1,6 +1,7 @@
import { useMemo, useRef, useState } from 'react'
import { Alert, Button, Card, Checkbox, Empty, Space, Tag, Typography } from '@arco-design/web-react'
import { IconCheckCircle, IconRobot } from '@arco-design/web-react/icon'
import { ApiError } from '../../api/client'
import type { InboxSuggestionDTO } from '../../api/inbox'
import type { InboxConfirmationOutcome, InboxItem, ProjectWorkspace } from './project-types'
@@ -76,7 +77,7 @@ export function ProjectInbox({ activeWorkspace, onAnalyze, onConfirm }: ProjectI
setSuccess(`已创建 ${outcome.createdCount} 个对象`)
setRefreshWarning(outcome.refreshError ?? '')
} catch (reason) {
setError(reason instanceof Error ? reason.message : '确认创建失败,请稍后重试')
setError(confirmationErrorMessage(reason))
} finally {
setConfirming(false)
}
@@ -191,7 +192,7 @@ export function ProjectInbox({ activeWorkspace, onAnalyze, onConfirm }: ProjectI
<Button
type="primary"
loading={confirming}
disabled={analyzing || isInboxItemProcessed(selectedItem, locallyConfirmedItemIds) || draftSuggestions.length === 0}
disabled={confirming || analyzing || isInboxItemProcessed(selectedItem, locallyConfirmedItemIds) || draftSuggestions.length === 0}
onClick={() => void confirmSelectedSuggestions()}
>
@@ -206,6 +207,13 @@ export function ProjectInbox({ activeWorkspace, onAnalyze, onConfirm }: ProjectI
)
}
function confirmationErrorMessage(reason: unknown) {
if (reason instanceof ApiError && (reason.code === 'network_error' || reason.code === 'invalid_response')) {
return '确认结果未知,服务端支持幂等处理,可安全重试'
}
return reason instanceof Error ? reason.message : '确认创建失败,请稍后重试'
}
function isInboxItemProcessed(item: InboxItem, locallyConfirmedItemIds: string[]) {
return item.status !== 'open' || locallyConfirmedItemIds.includes(item.id)
}