From fb4adbac765b08be7082484604e28e358b8b3e7e Mon Sep 17 00:00:00 2001 From: zhangheng Date: Wed, 22 Jul 2026 20:08:21 +0800 Subject: [PATCH] fix(feedback): make error retry stateful --- pages/feedback/index.ts | 94 ++++++++++++++++++++++++++++++--------- pages/feedback/index.wxml | 13 ++---- pages/feedback/index.wxss | 65 +++++++++++---------------- utils/api.ts | 4 +- 4 files changed, 103 insertions(+), 73 deletions(-) diff --git a/pages/feedback/index.ts b/pages/feedback/index.ts index 38e4598..6e03bb7 100644 --- a/pages/feedback/index.ts +++ b/pages/feedback/index.ts @@ -38,6 +38,7 @@ type PickerEvent = { detail: { value: string | number } } type InputEvent = { detail: { value: string } } type CheckboxGroupEvent = { detail: { value: string[] } } type LessonActionEvent = { currentTarget: { dataset: { lessonId: string } } } +type ErrorRetryAction = 'load' | 'generateSummary' interface PendingVoiceUpload { lessonId: string @@ -75,6 +76,7 @@ let voicePollTimer: number | null = null let summaryPollTimer: number | null = null let voicePollBusy = false let summaryPollBusy = false +let pendingSummaryIdempotencyKey: string | null = null let pageVisible = false let lessonStartedAt = 0 let segmentStartedAt = 0 @@ -196,6 +198,8 @@ Page({ session: null as FeedbackSession | null, contentLength: 0, loading: false, + errorRetrying: false, + errorRetryAction: 'load' as ErrorRetryAction, saving: false, summaryBusy: false, summaryRun: null as FeedbackSummaryRun | null, @@ -293,8 +297,11 @@ Page({ }) }, - async loadProfiles() { - this.setData({ loading: true, errorMessage: '', errorRequestId: '' }) + async loadProfiles(preserveError = false): Promise { + this.setData(preserveError + ? { loading: true } + : { loading: true, errorMessage: '', errorRequestId: '' }) + let success = false try { const profiles = await listProfiles() const matchedProfileIndex = profiles.findIndex((profile) => profile.id === this.data.draft.profileId) @@ -306,20 +313,25 @@ Page({ selectedProfileIndex, 'draft.profileId': profileId }) - await this.loadActiveSession(profileId) + success = await this.loadActiveSession(profileId) } catch (error) { - this.setData({ errorMessage: getApiErrorMessage(error), errorRequestId: getApiRequestId(error) }) + this.setData({ + errorMessage: getApiErrorMessage(error), + errorRequestId: getApiRequestId(error), + errorRetryAction: 'load' + }) } finally { this.setData({ loading: false }) } + return success }, - async loadActiveSession(profileId?: string | null) { + async loadActiveSession(profileId?: string | null): Promise { try { const targetProfileId = profileId === undefined ? this.data.draft.profileId : profileId const previousSessionId = this.data.session?.id || null const session = await getActiveFeedbackSession(targetProfileId) - if (targetProfileId !== this.data.draft.profileId) return + if (targetProfileId !== this.data.draft.profileId) return false const nextData: Record = { session, 'draft.feedbackSessionId': session?.id || null, @@ -337,8 +349,14 @@ Page({ this.syncVoicePresentation(session) this.syncSummaryRun(session?.latestSummaryRun || null) this.scheduleVoicePolling(session) + return true } catch (error) { - this.setData({ errorMessage: getApiErrorMessage(error), errorRequestId: getApiRequestId(error) }) + this.setData({ + errorMessage: getApiErrorMessage(error), + errorRequestId: getApiRequestId(error), + errorRetryAction: 'load' + }) + return false } }, @@ -401,10 +419,14 @@ Page({ this.scheduleSummaryPolling(run) }, - showApiError(error: unknown, showToast = true) { + showApiError( + error: unknown, + showToast = true, + retryAction: ErrorRetryAction = 'load' + ) { const errorMessage = getApiErrorMessage(error) const errorRequestId = getApiRequestId(error) - this.setData({ errorMessage, errorRequestId }) + this.setData({ errorMessage, errorRequestId, errorRetryAction: retryAction }) if (showToast) wx.showToast({ title: errorMessage, icon: 'none' }) }, @@ -486,8 +508,17 @@ Page({ } }, - retryLoad() { - void this.loadProfiles() + async retryError() { + if (this.data.loading || this.data.errorRetrying) return + this.setData({ errorRetrying: true }) + try { + const success = this.data.errorRetryAction === 'generateSummary' + ? await this.submitFeedbackSummary(true) + : await this.loadProfiles(true) + if (success) this.dismissError() + } finally { + this.setData({ errorRetrying: false }) + } }, copyRequestId() { @@ -495,7 +526,7 @@ Page({ }, dismissError() { - this.setData({ errorMessage: '', errorRequestId: '' }) + this.setData({ errorMessage: '', errorRequestId: '', errorRetryAction: 'load' }) }, onProfileChange(event: PickerEvent) { @@ -504,6 +535,7 @@ Page({ const selectedProfileIndex = Number(event.detail.value) const profile = this.data.profiles[selectedProfileIndex - 1] const profileId = profile?.id || null + pendingSummaryIdempotencyKey = null this.setData({ selectedProfileIndex, 'draft.profileId': profileId, @@ -901,7 +933,9 @@ Page({ try { await runWithSessionRefresh( async () => markVoiceLessonApplied(lesson.id, merged.content), - async () => this.loadActiveSession() + async () => { + await this.loadActiveSession() + } ) wx.showToast({ title: `第${lesson.sequence}节已加入`, icon: 'none' }) } catch (error) { @@ -932,28 +966,37 @@ Page({ this.syncVoicePresentation(this.data.session) }, - async generateFeedbackSummary() { + generateFeedbackSummary(): Promise { + pendingSummaryIdempotencyKey = null + return this.submitFeedbackSummary(false) + }, + + async submitFeedbackSummary(preserveError: boolean): Promise { const session = this.data.session - if (!session || this.data.summaryBusy || this.data.recording || this.data.voiceBusy) return + if (!session || this.data.summaryBusy || this.data.recording || this.data.voiceBusy) return false if (this.data.summaryRun?.status === 'queued' || this.data.summaryRun?.status === 'processing') { wx.showToast({ title: '当前总结仍在生成', icon: 'none' }) - return + return false } const lessonIds = this.data.voiceLessonViews .filter((lesson) => lesson.summarySelected) .map((lesson) => lesson.id) if (lessonIds.length === 0) { wx.showToast({ title: '请至少选择一节已就绪录音', icon: 'none' }) - return + return false } - this.setData({ summaryBusy: true, errorMessage: '', errorRequestId: '' }) + const idempotencyKey = pendingSummaryIdempotencyKey || createSummaryIdempotencyKey(session.id) + pendingSummaryIdempotencyKey = idempotencyKey + this.setData(preserveError + ? { summaryBusy: true } + : { summaryBusy: true, errorMessage: '', errorRequestId: '' }) try { const run = await createFeedbackSummaryRun( session.id, lessonIds, this.data.draft.content, - createSummaryIdempotencyKey(session.id) + idempotencyKey ) logVoiceOperation('feedback_summary_queued', { feedbackSessionId: session.id, @@ -961,9 +1004,12 @@ Page({ lessonIds }) this.syncSummaryRun(run) + pendingSummaryIdempotencyKey = null wx.showToast({ title: '已开始生成总结', icon: 'none' }) + return true } catch (error) { - this.showApiError(error) + this.showApiError(error, true, 'generateSummary') + return false } finally { this.setData({ summaryBusy: false }) } @@ -1026,7 +1072,9 @@ Page({ async () => { for (const segment of failedSegments) await retryVoiceSegment(segment.id) }, - async () => this.loadActiveSession() + async () => { + await this.loadActiveSession() + } ) logVoiceOperation('transcription_retry_submitted', { feedbackSessionId: session.id, @@ -1079,7 +1127,9 @@ Page({ async () => { for (const segment of failedSegments) await discardVoiceSegment(segment.id) }, - async () => this.loadActiveSession() + async () => { + await this.loadActiveSession() + } ) logVoiceOperation('failed_transcription_discarded', { feedbackSessionId: session.id, diff --git a/pages/feedback/index.wxml b/pages/feedback/index.wxml index ee371c3..1e98e70 100644 --- a/pages/feedback/index.wxml +++ b/pages/feedback/index.wxml @@ -6,19 +6,14 @@ 填写课堂表现并保存到教学反馈记录。