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

@@ -1,23 +1,35 @@
const BASE_URL = ''
class ApiClient {
async request<T>(url: string, options: RequestInit = {}): Promise<T> {
const response = await fetch(`${BASE_URL}${url}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '请求失败' }))
throw new Error(error.detail || '请求失败')
}
return response.json()
}
class ApiClient {
async request<T>(url: string, options: RequestInit = {}): Promise<T> {
const headers = new Headers(options.headers || {})
const isFormData = options.body instanceof FormData
if (!isFormData && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json')
}
const response = await fetch(`${BASE_URL}${url}`, {
...options,
credentials: 'include',
headers,
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '请求失败' }))
throw new Error(error.detail || '请求失败')
}
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

@@ -74,11 +74,7 @@ const latestJob = computed(() => jobs.value[0] ?? null)
const activeEvents = 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)
@@ -199,6 +195,7 @@ async function refresh() {
}
error.value = ''
const selectedJobId = activeJob.value?.id ?? null
try {
const [nextJobs, stats] = await Promise.all([
@@ -207,7 +204,11 @@ async function refresh() {
])
jobs.value = nextJobs
providerStats.value = stats
const nextJobId = jobs.value[0]?.id
const nextJobId = (
selectedJobId
? jobs.value.find((job) => job.id === selectedJobId)?.id
: null
) ?? jobs.value[0]?.id
if (nextJobId) {
await selectJob(nextJobId)
} else {
@@ -346,7 +347,13 @@ defineExpose({ refresh })
>
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-semibold">
{{ job.output_mode === 'asset_retry' ? '资源重试' : '内容生成' }}
{{
job.output_mode === 'asset_retry'
? '资源重试'
: job.output_mode === 'asset_generation'
? '资源生成'
: '内容生成'
}}
</span>
<span class="rounded-full border px-2 py-0.5 text-xs" :class="statusClass(job.status)">
{{ statusLabel(job.status) }}
@@ -366,7 +373,13 @@ defineExpose({ refresh })
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<div class="text-sm font-semibold">
{{ activeJob.output_mode === 'asset_retry' ? '资源重试事件' : '生成事件' }}
{{
activeJob.output_mode === 'asset_retry'
? '资源重试事件'
: activeJob.output_mode === 'asset_generation'
? '资源生成事件'
: '生成事件'
}}
</div>
<div class="mt-1 text-xs" :class="mutedClass">
当前步骤{{ eventLabel(activeJob.current_step) }}

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 = '/console/providers'
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)}`
}