fix: stabilize auth and generation workflows
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 || '生成失败,请稍后重试')
|
||||
}
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')
|
||||
|
||||
8
admin-frontend/src/utils/auth.ts
Normal file
8
admin-frontend/src/utils/auth.ts
Normal 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)}`
|
||||
}
|
||||
Reference in New Issue
Block a user