fix: stabilize auth and generation workflows

This commit is contained in:
2026-04-23 22:31:14 +08:00
parent 4db04e61e9
commit 7e450aa5fc
16 changed files with 335 additions and 127 deletions

View File

@@ -17,10 +17,19 @@ class ApiClient {
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '请求失败' }))
throw new Error(error.detail || '请求失败')
}
return response.json()
}
}
if (response.status === 204 || response.status === 205) {
return undefined as T
}
const contentType = response.headers.get('content-type') || ''
if (!contentType.includes('application/json')) {
return undefined as T
}
return response.json()
}
get<T>(url: string): Promise<T> {
return this.request<T>(url)

View File

@@ -145,12 +145,12 @@ function sleep(ms: number) {
async function waitForStoryId(jobId: string) {
for (let attempt = 0; attempt < JOB_POLL_MAX_ATTEMPTS; attempt += 1) {
const detail = await api.get<GenerationJobDetail>(`/api/generations/jobs/${jobId}`)
if (detail.status === 'canceled' || detail.current_step === 'generation_canceled') {
return null
}
if (detail.story_id) {
return detail.story_id
}
if (detail.status === 'canceled' || detail.current_step === 'generation_canceled') {
return null
}
if (detail.is_terminal) {
throw new Error(detail.error_message || '生成失败,请稍后重试')
}

View File

@@ -37,11 +37,7 @@ const latestJob = computed(() => jobHistory.value[0] ?? null)
const activeJobEvents = computed(() => activeJob.value?.events.slice(-10) ?? [])
const activeProgress = computed(() => activeJob.value?.progress_percent ?? latestJob.value?.progress_percent ?? 0)
const activeProgressLabel = computed(() => activeJob.value?.progress_label ?? latestJob.value?.progress_label ?? '暂无进度')
const shouldAutoRefresh = computed(() => {
if (activeJob.value) return !activeJob.value.is_terminal
if (latestJob.value) return !latestJob.value.is_terminal
return false
})
const shouldAutoRefresh = computed(() => Boolean(latestJob.value && !latestJob.value.is_terminal))
const providerSuccessRate = computed(() => {
if (!providerStats.value?.total_calls) return null
return Math.round((providerStats.value.successful_calls / providerStats.value.total_calls) * 100)
@@ -186,6 +182,7 @@ async function refresh() {
}
error.value = ''
const selectedJobId = activeJob.value?.id ?? null
try {
const [jobs, stats] = await Promise.all([
@@ -194,7 +191,11 @@ async function refresh() {
])
jobHistory.value = jobs
providerStats.value = stats
const nextJobId = jobHistory.value[0]?.id
const nextJobId = (
selectedJobId
? jobHistory.value.find((job) => job.id === selectedJobId)?.id
: null
) ?? jobHistory.value[0]?.id
if (nextJobId) {
await selectGenerationJob(nextJobId)
} else {

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { XMarkIcon, CommandLineIcon } from '@heroicons/vue/24/outline'
<script setup lang="ts">
import { XMarkIcon, CommandLineIcon } from '@heroicons/vue/24/outline'
import { buildAuthSigninUrl } from '../../utils/auth'
defineProps<{
modelValue: boolean
@@ -13,18 +14,18 @@ function close() {
emit('update:modelValue', false)
}
function loginWithGithub() {
window.location.href = '/auth/github/signin'
}
function loginWithGoogle() {
window.location.href = '/auth/google/signin'
}
function loginWithDev() {
window.location.href = '/auth/dev/signin'
}
</script>
function loginWithGithub() {
window.location.href = buildAuthSigninUrl('github')
}
function loginWithGoogle() {
window.location.href = buildAuthSigninUrl('google')
}
function loginWithDev() {
window.location.href = buildAuthSigninUrl('dev')
}
</script>
<template>
<Teleport to="body">

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { api } from '../api/client'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { api } from '../api/client'
import { buildAuthSigninUrl } from '../utils/auth'
interface User {
id: string
@@ -25,13 +26,13 @@ export const useUserStore = defineStore('user', () => {
}
}
function loginWithGithub() {
window.location.href = '/auth/github/signin'
}
function loginWithGoogle() {
window.location.href = '/auth/google/signin'
}
function loginWithGithub() {
window.location.href = buildAuthSigninUrl('github')
}
function loginWithGoogle() {
window.location.href = buildAuthSigninUrl('google')
}
async function logout() {
await api.post('/auth/signout')

View File

@@ -0,0 +1,8 @@
type AuthProvider = 'github' | 'google' | 'dev'
const DEFAULT_POST_LOGIN_PATH = '/my-stories'
export function buildAuthSigninUrl(provider: AuthProvider): string {
const next = new URL(DEFAULT_POST_LOGIN_PATH, window.location.origin).toString()
return `/auth/${provider}/signin?next=${encodeURIComponent(next)}`
}

View File

@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n'
import { useUserStore } from '../stores/user'
import { api } from '../api/client'
import type { VoiceSessionAnalytics, VoiceSessionSummary } from '../types/voiceSession'
import { getVoiceSessionNextAction } from '../utils/voiceSession'
import BaseButton from '../components/ui/BaseButton.vue'
import LoginDialog from '../components/ui/LoginDialog.vue'
import {
@@ -30,6 +31,9 @@ const showLoginDialog = ref(false)
const activeVoiceSession = ref<VoiceSessionSummary | null>(null)
const voiceAnalytics = ref<VoiceSessionAnalytics | null>(null)
type VoiceAttentionReason = 'pending_confirmation' | 'safety_intervention' | 'failed_turn'
type VoiceStudioFocusTarget = 'confirmation' | 'safety' | 'failed' | 'text'
// ========== 创作入口 ==========
// 旧的创作变量已移除,现在只负责跳转
function openCreateModal() {
@@ -54,7 +58,23 @@ function continueVoiceStudio() {
openVoiceStudio()
return
}
router.push('/voice-studio')
const action = getVoiceSessionNextAction(activeVoiceSession.value)
if (action.storyId) {
router.push(`/story/${action.storyId}`)
return
}
const query: Record<string, string> = {
session: activeVoiceSession.value.id,
}
if (action.reason) {
query.filter = 'attention'
query.reason = action.reason as VoiceAttentionReason
}
if (action.focus) {
query.focus = action.focus as VoiceStudioFocusTarget
}
router.push({ path: '/voice-studio', query })
}
async function loadActiveVoiceSession() {

View File

@@ -537,6 +537,10 @@ function resolveDisplayedSessions(sourceSessions: VoiceSessionSummary[]) {
return sortDisplayedSessions(visibleSessions)
}
function isSessionVisibleInCurrentFilter(sessionId: string) {
return resolveDisplayedSessions(sessions.value).some((session) => session.id === sessionId)
}
function parseSessionFilter(value: unknown): SessionFilter | null {
if (value === 'active' || value === 'attention' || value === 'recent') {
return value
@@ -743,10 +747,17 @@ async function loadSessions() {
const previousActiveSession = activeSession.value
sessions.value = await api.get<VoiceSessionSummary[]>(buildVoiceSessionListPath())
const displayedSessions = resolveDisplayedSessions(sessions.value)
const hiddenRequestedSession = requestedSessionId.value
? sessions.value.find((item) => item.id === requestedSessionId.value) ?? null
: null
const hiddenCurrentSession = previousActiveSession
? sessions.value.find((item) => item.id === previousActiveSession.id) ?? null
: null
if (
(requestedSessionId.value || pendingFocusTarget.value)
&& requestedSessionId.value
&& !displayedSessions.some((item) => item.id === requestedSessionId.value)
&& !hiddenRequestedSession
) {
void syncVoiceStudioRouteState()
}
@@ -754,7 +765,10 @@ async function loadSessions() {
requestedSessionId.value
? displayedSessions.find((item) => item.id === requestedSessionId.value)
: null
) ?? displayedSessions.find((item) => item.can_continue) ?? displayedSessions[0]
) ?? displayedSessions.find((item) => item.can_continue) ?? displayedSessions[0] ?? hiddenRequestedSession ?? hiddenCurrentSession
const currentSessionStillAvailable = activeSession.value
? sessions.value.some((item) => item.id === activeSession.value?.id)
: false
const currentSessionStillVisible = activeSession.value
? displayedSessions.some((item) => item.id === activeSession.value?.id)
: false
@@ -782,6 +796,9 @@ async function loadSessions() {
}
await loadSessionDetail(preferredSession.id)
} else if (sessionFilter.value !== 'recent') {
if (currentSessionStillAvailable) {
return
}
if (
sessionFilter.value === 'attention'
&& previousActiveSession
@@ -815,6 +832,16 @@ async function loadLatestActiveSession() {
try {
const session = await api.get<VoiceSessionSummary | null>('/api/voice-sessions/active')
if (session) {
if (
!requestedSessionId.value
&& !route.query.filter
&& session.attention_reasons.length > 0
) {
const action = getVoiceSessionNextAction(session)
sessionFilter.value = 'attention'
attentionReasonFilter.value = action.reason ?? 'all'
pendingFocusTarget.value = action.focus ?? pendingFocusTarget.value
}
await loadSessionDetail(session.id)
}
} catch {
@@ -845,13 +872,18 @@ function stopSessionPolling() {
function startSessionPolling() {
if (!activeSession.value?.id || sessionPollTimer) return
sessionPollTimer = window.setInterval(() => {
if (activeSession.value?.id) {
void loadSessionDetail(activeSession.value.id)
void loadSessions()
const sessionId = activeSession.value?.id
if (sessionId) {
void refreshVisibleSessionState(sessionId)
}
}, sessionPollIntervalMs)
}
async function refreshVisibleSessionState(sessionId: string) {
await loadSessionDetail(sessionId)
await loadSessions()
}
async function createSession() {
creatingSession.value = true
error.value = ''
@@ -942,11 +974,12 @@ async function submitRecordedTurn() {
async function finalizeSession() {
if (!activeSession.value) return
const sessionId = activeSession.value.id
finalizing.value = true
error.value = ''
try {
await api.post<VoiceSessionFinalizeResponse>(
`/api/voice-sessions/${activeSession.value.id}/finalize`,
const result = await api.post<VoiceSessionFinalizeResponse>(
`/api/voice-sessions/${sessionId}/finalize`,
{
save_story: true,
generate_cover: true,
@@ -954,7 +987,12 @@ async function finalizeSession() {
},
)
await loadSessions()
await loadSessionDetail(activeSession.value.id)
if (isSessionVisibleInCurrentFilter(sessionId)) {
await loadSessionDetail(sessionId)
} else if (result.story_id) {
router.push(`/story/${result.story_id}`)
return
}
await loadVoiceAnalytics()
} catch (err) {
error.value = err instanceof Error ? err.message : '保存语音共创故事失败'
@@ -1026,17 +1064,17 @@ async function resolveTurnConfirmation(turn: VoiceTurnSummary, action: 'accept'
async function abandonSession() {
if (!activeSession.value) return
const sessionId = activeSession.value.id
abandoning.value = true
error.value = ''
try {
const summary = await api.post<VoiceSessionSummary>(
`/api/voice-sessions/${activeSession.value.id}/abandon`,
await api.post<VoiceSessionSummary>(
`/api/voice-sessions/${sessionId}/abandon`,
{ reason: '用户在语音共创页主动结束会话' },
)
await loadSessions()
activeSession.value = {
...(activeSession.value as VoiceSessionDetail),
...summary,
if (isSessionVisibleInCurrentFilter(sessionId)) {
await loadSessionDetail(sessionId)
}
} catch (err) {
error.value = err instanceof Error ? err.message : '放弃会话失败'