- Add debug_font_path, demo_docs_dir, e2e_api_base_url to config.py - Fix hardcoded paths in pp_structure_debug.py, create_demo_images.py - Fix hardcoded paths in test files - Update .env.example with new configuration options - Update .gitignore to exclude AI development files (.claude/, openspec/, AGENTS.md, CLAUDE.md) - Add production startup script (start-prod.sh) - Add README.md with project documentation - Add 1panel Docker deployment files (docker-compose.yml, Dockerfiles, nginx.conf) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
823 lines
23 KiB
TypeScript
823 lines
23 KiB
TypeScript
/**
|
|
* API V2 Client - External Authentication & Task Management
|
|
*
|
|
* Features:
|
|
* - External Azure AD authentication
|
|
* - Task history and management
|
|
* - User task isolation
|
|
* - Session management
|
|
*/
|
|
|
|
import axios, { AxiosError } from 'axios'
|
|
import type { AxiosInstance } from 'axios'
|
|
import type {
|
|
LoginRequest,
|
|
ApiError,
|
|
LoginResponseV2,
|
|
UserInfo,
|
|
TaskCreate,
|
|
TaskUpdate,
|
|
Task,
|
|
TaskDetail,
|
|
TaskListResponse,
|
|
TaskStats,
|
|
SessionInfo,
|
|
SystemStats,
|
|
UserWithStats,
|
|
TopUser,
|
|
AuditLogListResponse,
|
|
UserActivitySummary,
|
|
TranslationStats,
|
|
ProcessingOptions,
|
|
ProcessingMetadata,
|
|
DocumentAnalysisResponse,
|
|
PreprocessingPreviewRequest,
|
|
PreprocessingPreviewResponse,
|
|
TranslationRequest,
|
|
TranslationStartResponse,
|
|
TranslationStatusResponse,
|
|
TranslationListResponse,
|
|
TranslationResult,
|
|
ExportRule,
|
|
StorageStats,
|
|
CleanupResult,
|
|
AdminTaskListResponse,
|
|
} from '@/types/apiV2'
|
|
|
|
/**
|
|
* API Client Configuration
|
|
* - In Docker: VITE_API_BASE_URL is empty string, use relative path
|
|
* - In development: Prefer relative path + Vite proxy; optionally set VITE_API_BASE_URL for non-proxied setups
|
|
*/
|
|
const envApiBaseUrl = import.meta.env.VITE_API_BASE_URL
|
|
const API_BASE_URL = envApiBaseUrl !== undefined ? envApiBaseUrl : ''
|
|
const API_VERSION = 'v2'
|
|
|
|
class ApiClientV2 {
|
|
private client: AxiosInstance
|
|
private token: string | null = null
|
|
private userInfo: UserInfo | null = null
|
|
private tokenExpiresAt: number | null = null
|
|
private refreshTimer: ReturnType<typeof setTimeout> | null = null
|
|
private isRefreshing: boolean = false
|
|
private refreshFailed: boolean = false
|
|
|
|
constructor() {
|
|
this.client = axios.create({
|
|
baseURL: `${API_BASE_URL}/api/${API_VERSION}`,
|
|
timeout: 30000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
|
|
// Request interceptor to add auth token
|
|
this.client.interceptors.request.use(
|
|
(config) => {
|
|
if (this.token) {
|
|
config.headers.Authorization = `Bearer ${this.token}`
|
|
}
|
|
return config
|
|
},
|
|
(error) => Promise.reject(error)
|
|
)
|
|
|
|
// Response interceptor for error handling
|
|
this.client.interceptors.response.use(
|
|
(response) => response,
|
|
async (error: AxiosError<ApiError>) => {
|
|
if (error.response?.status === 401) {
|
|
// If refresh has already failed, don't try again - just redirect
|
|
if (this.refreshFailed) {
|
|
console.warn('Refresh already failed, redirecting to login')
|
|
this.clearAuth()
|
|
window.location.href = '/login'
|
|
return Promise.reject(error)
|
|
}
|
|
|
|
// If already refreshing, reject immediately to prevent retry storm
|
|
if (this.isRefreshing) {
|
|
console.warn('Token refresh already in progress, rejecting request')
|
|
return Promise.reject(error)
|
|
}
|
|
|
|
// Token expired or invalid
|
|
const detail = error.response?.data?.detail
|
|
if (detail?.includes('Session expired') || detail?.includes('Invalid session')) {
|
|
console.warn('Session expired, attempting refresh')
|
|
this.isRefreshing = true
|
|
|
|
try {
|
|
await this.refreshToken()
|
|
this.isRefreshing = false
|
|
|
|
// Retry the original request only if refresh succeeded
|
|
if (error.config) {
|
|
return this.client.request(error.config)
|
|
}
|
|
} catch (refreshError) {
|
|
console.error('Token refresh failed, redirecting to login')
|
|
this.isRefreshing = false
|
|
this.refreshFailed = true
|
|
this.clearAuth()
|
|
window.location.href = '/login'
|
|
return Promise.reject(error)
|
|
}
|
|
} else {
|
|
this.clearAuth()
|
|
window.location.href = '/login'
|
|
}
|
|
}
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
// Load auth data from localStorage
|
|
this.loadAuth()
|
|
}
|
|
|
|
/**
|
|
* Set authentication data
|
|
*/
|
|
setAuth(token: string, user: UserInfo, expiresIn?: number) {
|
|
this.token = token
|
|
this.userInfo = user
|
|
localStorage.setItem('auth_token_v2', token)
|
|
localStorage.setItem('user_info_v2', JSON.stringify(user))
|
|
|
|
// Schedule token refresh if expiresIn is provided
|
|
if (expiresIn) {
|
|
this.tokenExpiresAt = Date.now() + expiresIn * 1000
|
|
localStorage.setItem('token_expires_at', this.tokenExpiresAt.toString())
|
|
this.scheduleTokenRefresh(expiresIn)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear authentication data
|
|
*/
|
|
clearAuth() {
|
|
this.token = null
|
|
this.userInfo = null
|
|
this.tokenExpiresAt = null
|
|
this.isRefreshing = false
|
|
this.refreshFailed = false
|
|
|
|
// Clear refresh timer
|
|
if (this.refreshTimer) {
|
|
clearTimeout(this.refreshTimer)
|
|
this.refreshTimer = null
|
|
}
|
|
|
|
localStorage.removeItem('auth_token_v2')
|
|
localStorage.removeItem('user_info_v2')
|
|
localStorage.removeItem('token_expires_at')
|
|
}
|
|
|
|
/**
|
|
* Load auth data from localStorage
|
|
*/
|
|
private loadAuth() {
|
|
const token = localStorage.getItem('auth_token_v2')
|
|
const userInfoStr = localStorage.getItem('user_info_v2')
|
|
const expiresAtStr = localStorage.getItem('token_expires_at')
|
|
|
|
if (token && userInfoStr) {
|
|
try {
|
|
this.token = token
|
|
this.userInfo = JSON.parse(userInfoStr)
|
|
|
|
// Load and check token expiry
|
|
if (expiresAtStr) {
|
|
this.tokenExpiresAt = parseInt(expiresAtStr, 10)
|
|
const timeUntilExpiry = this.tokenExpiresAt - Date.now()
|
|
|
|
// If token is expired, clear auth
|
|
if (timeUntilExpiry <= 0) {
|
|
console.warn('Token expired, clearing auth')
|
|
this.clearAuth()
|
|
return
|
|
}
|
|
|
|
// Schedule refresh if token is expiring soon
|
|
const refreshBuffer = 5 * 60 * 1000 // 5 minutes
|
|
if (timeUntilExpiry < refreshBuffer) {
|
|
console.log('Token expiring soon, refreshing immediately')
|
|
this.refreshToken().catch(() => this.clearAuth())
|
|
} else {
|
|
// Schedule refresh for later
|
|
this.scheduleTokenRefresh(Math.floor(timeUntilExpiry / 1000))
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to parse user info from localStorage:', error)
|
|
this.clearAuth()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user is authenticated
|
|
*/
|
|
isAuthenticated(): boolean {
|
|
return this.token !== null && this.userInfo !== null
|
|
}
|
|
|
|
/**
|
|
* Get current user info
|
|
*/
|
|
getCurrentUser(): UserInfo | null {
|
|
return this.userInfo
|
|
}
|
|
|
|
/**
|
|
* Schedule token refresh before expiration
|
|
* @param expiresIn - Token expiry time in seconds
|
|
*/
|
|
private scheduleTokenRefresh(expiresIn: number): void {
|
|
// Clear existing timer
|
|
if (this.refreshTimer) {
|
|
clearTimeout(this.refreshTimer)
|
|
}
|
|
|
|
// Schedule refresh 5 minutes before expiry
|
|
const refreshBuffer = 5 * 60 // 5 minutes in seconds
|
|
const refreshTime = Math.max(0, expiresIn - refreshBuffer) * 1000 // Convert to milliseconds
|
|
|
|
console.log(`Scheduling token refresh in ${refreshTime / 1000} seconds`)
|
|
|
|
this.refreshTimer = setTimeout(() => {
|
|
console.log('Auto-refreshing token')
|
|
this.refreshToken().catch((error) => {
|
|
console.error('Auto token refresh failed:', error)
|
|
// Don't redirect on auto-refresh failure, let user continue
|
|
// Redirect will happen on next API call with 401
|
|
})
|
|
}, refreshTime)
|
|
}
|
|
|
|
/**
|
|
* Refresh access token
|
|
*/
|
|
private async refreshToken(): Promise<void> {
|
|
try {
|
|
const response = await this.client.post<LoginResponseV2>('/auth/refresh')
|
|
|
|
// Update token and schedule next refresh
|
|
this.setAuth(response.data.access_token, response.data.user, response.data.expires_in)
|
|
|
|
console.log('Token refreshed successfully')
|
|
} catch (error) {
|
|
console.error('Token refresh failed:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// ==================== Authentication ====================
|
|
|
|
/**
|
|
* Login via external Azure AD API
|
|
*/
|
|
async login(data: LoginRequest): Promise<LoginResponseV2> {
|
|
const response = await this.client.post<LoginResponseV2>('/auth/login', {
|
|
username: data.username,
|
|
password: data.password,
|
|
})
|
|
|
|
// Store token and user info with auto-refresh
|
|
this.setAuth(response.data.access_token, response.data.user, response.data.expires_in)
|
|
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Logout (invalidate session)
|
|
*/
|
|
async logout(sessionId?: number): Promise<void> {
|
|
try {
|
|
await this.client.post('/auth/logout', { session_id: sessionId })
|
|
} finally {
|
|
// Always clear local auth data
|
|
this.clearAuth()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current user info from server
|
|
*/
|
|
async getMe(): Promise<UserInfo> {
|
|
const response = await this.client.get<UserInfo>('/auth/me')
|
|
this.userInfo = response.data
|
|
localStorage.setItem('user_info_v2', JSON.stringify(response.data))
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* List user sessions
|
|
*/
|
|
async listSessions(): Promise<SessionInfo[]> {
|
|
const response = await this.client.get<{ sessions: SessionInfo[] }>('/auth/sessions')
|
|
return response.data.sessions
|
|
}
|
|
|
|
// ==================== File Upload ====================
|
|
|
|
/**
|
|
* Upload a file
|
|
*/
|
|
async uploadFile(file: File): Promise<{ task_id: string; filename: string; file_size: number; file_type: string; status: string }> {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
const response = await this.client.post('/upload', formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
})
|
|
|
|
return response.data
|
|
}
|
|
|
|
// ==================== Task Management ====================
|
|
|
|
/**
|
|
* Create a new task
|
|
*/
|
|
async createTask(data: TaskCreate): Promise<Task> {
|
|
const response = await this.client.post<Task>('/tasks/', data)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* List tasks with pagination and filtering
|
|
*/
|
|
async listTasks(params: {
|
|
status?: 'pending' | 'processing' | 'completed' | 'failed'
|
|
filename?: string
|
|
date_from?: string
|
|
date_to?: string
|
|
page?: number
|
|
page_size?: number
|
|
order_by?: string
|
|
order_desc?: boolean
|
|
} = {}): Promise<TaskListResponse> {
|
|
const response = await this.client.get<TaskListResponse>('/tasks/', { params })
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get task statistics
|
|
*/
|
|
async getTaskStats(): Promise<TaskStats> {
|
|
const response = await this.client.get<TaskStats>('/tasks/stats')
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get task details by ID
|
|
*/
|
|
async getTask(taskId: string): Promise<TaskDetail> {
|
|
const response = await this.client.get<TaskDetail>(`/tasks/${taskId}`)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Update task
|
|
*/
|
|
async updateTask(taskId: string, data: TaskUpdate): Promise<Task> {
|
|
const response = await this.client.patch<Task>(`/tasks/${taskId}`, data)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Delete task
|
|
*/
|
|
async deleteTask(taskId: string): Promise<void> {
|
|
await this.client.delete(`/tasks/${taskId}`)
|
|
}
|
|
|
|
/**
|
|
* Start task processing with optional dual-track settings and PP-StructureV3 parameters
|
|
*/
|
|
async startTask(taskId: string, options?: ProcessingOptions): Promise<Task> {
|
|
// Send full options object in request body (not query params)
|
|
// Backend will use defaults for any unspecified fields
|
|
const body = options || {
|
|
use_dual_track: true,
|
|
language: 'ch'
|
|
}
|
|
|
|
const response = await this.client.post<Task>(`/tasks/${taskId}/start`, body)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Analyze document to get recommended processing track
|
|
*/
|
|
async analyzeDocument(taskId: string): Promise<DocumentAnalysisResponse> {
|
|
const response = await this.client.post<DocumentAnalysisResponse>(`/tasks/${taskId}/analyze`)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get processing metadata for a completed task
|
|
*/
|
|
async getProcessingMetadata(taskId: string): Promise<ProcessingMetadata> {
|
|
const response = await this.client.get<ProcessingMetadata>(`/tasks/${taskId}/metadata`)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Cancel task
|
|
*/
|
|
async cancelTask(taskId: string): Promise<Task> {
|
|
const response = await this.client.post<Task>(`/tasks/${taskId}/cancel`)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Retry failed task
|
|
*/
|
|
async retryTask(taskId: string): Promise<Task> {
|
|
const response = await this.client.post<Task>(`/tasks/${taskId}/retry`)
|
|
return response.data
|
|
}
|
|
|
|
// ==================== Helper Methods ====================
|
|
|
|
/**
|
|
* Download file from task result
|
|
*/
|
|
async downloadTaskFile(url: string, filename: string): Promise<void> {
|
|
const response = await this.client.get(url, {
|
|
responseType: 'blob',
|
|
})
|
|
|
|
// Create download link
|
|
const blob = new Blob([response.data])
|
|
const link = document.createElement('a')
|
|
link.href = window.URL.createObjectURL(blob)
|
|
link.download = filename
|
|
link.click()
|
|
window.URL.revokeObjectURL(link.href)
|
|
}
|
|
|
|
/**
|
|
* Download task result as JSON
|
|
*/
|
|
async downloadJSON(taskId: string): Promise<void> {
|
|
const response = await this.client.get(`/tasks/${taskId}/download/json`, {
|
|
responseType: 'blob',
|
|
})
|
|
|
|
const blob = new Blob([response.data], { type: 'application/json' })
|
|
const link = document.createElement('a')
|
|
link.href = window.URL.createObjectURL(blob)
|
|
link.download = `${taskId}_result.json`
|
|
link.click()
|
|
window.URL.revokeObjectURL(link.href)
|
|
}
|
|
|
|
/**
|
|
* Download task result as Markdown
|
|
*/
|
|
async downloadMarkdown(taskId: string): Promise<void> {
|
|
const response = await this.client.get(`/tasks/${taskId}/download/markdown`, {
|
|
responseType: 'blob',
|
|
})
|
|
|
|
const blob = new Blob([response.data], { type: 'text/markdown' })
|
|
const link = document.createElement('a')
|
|
link.href = window.URL.createObjectURL(blob)
|
|
link.download = `${taskId}_result.md`
|
|
link.click()
|
|
window.URL.revokeObjectURL(link.href)
|
|
}
|
|
|
|
/**
|
|
* Download task result as PDF
|
|
* @param taskId - The task ID
|
|
* @param format - PDF format: 'layout' (default) or 'reflow'
|
|
*/
|
|
async downloadPDF(taskId: string, format: 'layout' | 'reflow' = 'layout'): Promise<void> {
|
|
const response = await this.client.get(`/tasks/${taskId}/download/pdf`, {
|
|
params: format === 'reflow' ? { format: 'reflow' } : undefined,
|
|
responseType: 'blob',
|
|
})
|
|
|
|
const blob = new Blob([response.data], { type: 'application/pdf' })
|
|
const link = document.createElement('a')
|
|
link.href = window.URL.createObjectURL(blob)
|
|
const formatSuffix = format === 'reflow' ? '_reflow' : '_layout'
|
|
link.download = `${taskId}${formatSuffix}.pdf`
|
|
link.click()
|
|
window.URL.revokeObjectURL(link.href)
|
|
}
|
|
|
|
/**
|
|
* Download task result as UnifiedDocument JSON
|
|
*/
|
|
async downloadUnified(taskId: string): Promise<void> {
|
|
const response = await this.client.get(`/tasks/${taskId}/download/unified`, {
|
|
responseType: 'blob',
|
|
})
|
|
|
|
const blob = new Blob([response.data], { type: 'application/json' })
|
|
const link = document.createElement('a')
|
|
link.href = window.URL.createObjectURL(blob)
|
|
link.download = `${taskId}_unified.json`
|
|
link.click()
|
|
window.URL.revokeObjectURL(link.href)
|
|
}
|
|
|
|
/**
|
|
* Download visualization images as ZIP (OCR Track only)
|
|
*/
|
|
async downloadVisualization(taskId: string): Promise<void> {
|
|
const response = await this.client.get(`/tasks/${taskId}/download/visualization`, {
|
|
responseType: 'blob',
|
|
})
|
|
|
|
const blob = new Blob([response.data], { type: 'application/zip' })
|
|
const link = document.createElement('a')
|
|
link.href = window.URL.createObjectURL(blob)
|
|
link.download = `${taskId}_visualization.zip`
|
|
link.click()
|
|
window.URL.revokeObjectURL(link.href)
|
|
}
|
|
|
|
// ==================== Preprocessing Preview APIs ====================
|
|
|
|
/**
|
|
* Preview preprocessing effect on a page
|
|
*/
|
|
async previewPreprocessing(
|
|
taskId: string,
|
|
request: PreprocessingPreviewRequest
|
|
): Promise<PreprocessingPreviewResponse> {
|
|
const response = await this.client.post<PreprocessingPreviewResponse>(
|
|
`/tasks/${taskId}/preview/preprocessing`,
|
|
request
|
|
)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get preprocessing preview image URL (with auth token)
|
|
*/
|
|
getPreviewImageUrl(taskId: string, type: 'original' | 'preprocessed', page: number): string {
|
|
const baseUrl = `${API_BASE_URL}/api/${API_VERSION}/tasks/${taskId}/preview/image`
|
|
return `${baseUrl}?type=${type}&page=${page}`
|
|
}
|
|
|
|
/**
|
|
* Get preview image as blob (with authentication)
|
|
*/
|
|
async getPreviewImage(taskId: string, type: 'original' | 'preprocessed', page: number): Promise<Blob> {
|
|
const response = await this.client.get(`/tasks/${taskId}/preview/image`, {
|
|
params: { type, page },
|
|
responseType: 'blob',
|
|
})
|
|
return response.data
|
|
}
|
|
|
|
// ==================== Admin APIs ====================
|
|
|
|
/**
|
|
* Get system statistics (admin only)
|
|
*/
|
|
async getSystemStats(): Promise<SystemStats> {
|
|
const response = await this.client.get<SystemStats>('/admin/stats')
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get user list with statistics (admin only)
|
|
*/
|
|
async listUsers(params: {
|
|
page?: number
|
|
page_size?: number
|
|
} = {}): Promise<{ users: UserWithStats[]; total: number; page: number; page_size: number }> {
|
|
const response = await this.client.get('/admin/users', { params })
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get top users by metric (admin only)
|
|
*/
|
|
async getTopUsers(params: {
|
|
metric?: 'tasks' | 'completed_tasks'
|
|
limit?: number
|
|
} = {}): Promise<TopUser[]> {
|
|
const response = await this.client.get<TopUser[]>('/admin/users/top', { params })
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get audit logs (admin only)
|
|
*/
|
|
async getAuditLogs(params: {
|
|
user_id?: number
|
|
category?: string
|
|
action?: string
|
|
success?: boolean
|
|
date_from?: string
|
|
date_to?: string
|
|
page?: number
|
|
page_size?: number
|
|
} = {}): Promise<AuditLogListResponse> {
|
|
const response = await this.client.get<AuditLogListResponse>('/admin/audit-logs', { params })
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get user activity summary (admin only)
|
|
*/
|
|
async getUserActivitySummary(userId: number, days: number = 30): Promise<UserActivitySummary> {
|
|
const response = await this.client.get<UserActivitySummary>(
|
|
`/admin/audit-logs/user/${userId}/summary`,
|
|
{ params: { days } }
|
|
)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get translation statistics (admin only)
|
|
*/
|
|
async getTranslationStats(): Promise<TranslationStats> {
|
|
const response = await this.client.get<TranslationStats>('/admin/translation-stats')
|
|
return response.data
|
|
}
|
|
|
|
// ==================== Translation APIs ====================
|
|
|
|
/**
|
|
* Start a translation job
|
|
*/
|
|
async startTranslation(taskId: string, request: TranslationRequest): Promise<TranslationStartResponse> {
|
|
const response = await this.client.post<TranslationStartResponse>(
|
|
`/translate/${taskId}`,
|
|
request
|
|
)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get translation status
|
|
*/
|
|
async getTranslationStatus(taskId: string): Promise<TranslationStatusResponse> {
|
|
const response = await this.client.get<TranslationStatusResponse>(
|
|
`/translate/${taskId}/status`
|
|
)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get translation result
|
|
*/
|
|
async getTranslationResult(taskId: string, lang: string): Promise<TranslationResult> {
|
|
const response = await this.client.get<TranslationResult>(
|
|
`/translate/${taskId}/result`,
|
|
{ params: { lang } }
|
|
)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Download translation result as JSON file
|
|
*/
|
|
async downloadTranslation(taskId: string, lang: string): Promise<void> {
|
|
const response = await this.client.get(`/translate/${taskId}/result`, {
|
|
params: { lang },
|
|
responseType: 'blob',
|
|
})
|
|
|
|
const blob = new Blob([response.data], { type: 'application/json' })
|
|
const link = document.createElement('a')
|
|
link.href = window.URL.createObjectURL(blob)
|
|
link.download = `${taskId}_translated_${lang}.json`
|
|
link.click()
|
|
window.URL.revokeObjectURL(link.href)
|
|
}
|
|
|
|
/**
|
|
* List available translations for a task
|
|
*/
|
|
async listTranslations(taskId: string): Promise<TranslationListResponse> {
|
|
const response = await this.client.get<TranslationListResponse>(
|
|
`/translate/${taskId}/translations`
|
|
)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Delete a translation
|
|
*/
|
|
async deleteTranslation(taskId: string, lang: string): Promise<void> {
|
|
await this.client.delete(`/translate/${taskId}/translations/${lang}`)
|
|
}
|
|
|
|
/**
|
|
* Download translated PDF
|
|
* @param taskId - The task ID
|
|
* @param lang - Target language code
|
|
* @param format - PDF format: 'layout' or 'reflow' (default: 'reflow')
|
|
*/
|
|
async downloadTranslatedPdf(
|
|
taskId: string,
|
|
lang: string,
|
|
format: 'layout' | 'reflow' = 'reflow'
|
|
): Promise<void> {
|
|
const response = await this.client.post(`/translate/${taskId}/pdf`, null, {
|
|
params: { lang, format },
|
|
responseType: 'blob',
|
|
})
|
|
|
|
const formatSuffix = format === 'layout' ? '_layout' : '_reflow'
|
|
const blob = new Blob([response.data], { type: 'application/pdf' })
|
|
const link = document.createElement('a')
|
|
link.href = window.URL.createObjectURL(blob)
|
|
link.download = `${taskId}_translated_${lang}${formatSuffix}.pdf`
|
|
link.click()
|
|
window.URL.revokeObjectURL(link.href)
|
|
}
|
|
|
|
// ==================== Export Rules APIs ====================
|
|
|
|
/**
|
|
* Get export rules
|
|
*/
|
|
async getExportRules(): Promise<ExportRule[]> {
|
|
const response = await this.client.get<ExportRule[]>('/export/rules')
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Create export rule
|
|
*/
|
|
async createExportRule(rule: Omit<ExportRule, 'id' | 'created_at'>): Promise<ExportRule> {
|
|
const response = await this.client.post<ExportRule>('/export/rules', rule)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Update export rule
|
|
*/
|
|
async updateExportRule(ruleId: number, rule: Partial<ExportRule>): Promise<ExportRule> {
|
|
const response = await this.client.put<ExportRule>(`/export/rules/${ruleId}`, rule)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Delete export rule
|
|
*/
|
|
async deleteExportRule(ruleId: number): Promise<void> {
|
|
await this.client.delete(`/export/rules/${ruleId}`)
|
|
}
|
|
|
|
// ==================== Admin Storage Management ====================
|
|
|
|
/**
|
|
* Get storage statistics (admin only)
|
|
*/
|
|
async getStorageStats(): Promise<StorageStats> {
|
|
const response = await this.client.get<StorageStats>('/admin/storage/stats')
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Trigger file cleanup (admin only)
|
|
*/
|
|
async triggerCleanup(maxFilesPerUser?: number): Promise<CleanupResult> {
|
|
const params = maxFilesPerUser ? { max_files_per_user: maxFilesPerUser } : {}
|
|
const response = await this.client.post<CleanupResult>('/admin/cleanup/trigger', null, { params })
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* List all tasks (admin only)
|
|
*/
|
|
async listAllTasksAdmin(params: {
|
|
user_id?: number
|
|
status_filter?: string
|
|
include_deleted?: boolean
|
|
include_files_deleted?: boolean
|
|
page?: number
|
|
page_size?: number
|
|
}): Promise<AdminTaskListResponse> {
|
|
const response = await this.client.get<AdminTaskListResponse>('/admin/tasks', { params })
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Get task details (admin only, can view any task including deleted)
|
|
*/
|
|
async getTaskAdmin(taskId: string): Promise<Task> {
|
|
const response = await this.client.get<Task>(`/admin/tasks/${taskId}`)
|
|
return response.data
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const apiClientV2 = new ApiClientV2()
|