import type { Annotation, AuditLogEntry, CurrentUser, CustomerAccessState, FeedbackReply, FeedbackType, ManagedApiKey, ManagedUser, Note, NoteDetail, NoteListQuery, OperationGroup, Project, ReviewStatus, StorageConfig, TextAnnotation, UserRole, WorkComment, WorkFeedbackBundle } from '@shared/types'; export class ApiError extends Error { constructor(public status: number, message: string) { super(message); this.name = 'ApiError'; } } async function request(url: string, init?: RequestInit): Promise { const res = await fetch(url, init); if (!res.ok) { let message = `请求失败 (${res.status})`; try { const data = await res.json(); message = data.error ?? data.message ?? message; } catch { /* noop */ } throw new ApiError(res.status, message); } if (res.status === 204) return undefined as T; return res.json(); } export const api = { login: (username: string, password: string) => request<{ success: true }>('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }), logout: () => request('/api/auth/logout', { method: 'POST' }), me: () => request('/api/auth/me'), changePassword: (currentPassword: string, newPassword: string) => request<{ success: true }>('/api/auth/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPassword, newPassword }) }), updateCurrentGroup: (name: string) => request<{ id: number; name: string }>('/api/groups/current', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }), listGroups: () => request('/api/management/groups'), createGroup: (data: { name: string; username: string; display_name: string; password: string }) => request('/api/management/groups', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), setGroupStatus: (id: number, status: 'active' | 'disabled') => request<{ success: true; status: string }>(`/api/management/groups/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }), updateGroupName: (id: number, name: string) => request<{ id: number; name: string }>(`/api/management/groups/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }), replaceGroupAdmin: (id: number, user_id: number, previous_action: 'demote' | 'disable') => request<{ success: true; previous_admin: string; next_admin: string }>(`/api/management/groups/${id}/replace-admin`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id, previous_action }) }), listManagedUsers: (groupId?: number) => request(`/api/management/users${groupId ? `?groupId=${groupId}` : ''}`), createManagedUser: (data: { group_id?: number; username: string; display_name: string; password: string; role?: UserRole }) => request('/api/management/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), updateManagedUserName: (id: number, display_name: string) => request<{ success: true; display_name: string }>(`/api/management/users/${id}/name`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ display_name }) }), setUserStatus: (id: number, status: 'active' | 'disabled') => request<{ success: true; status: string }>(`/api/management/users/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }), resetUserPassword: (id: number, password: string) => request<{ success: true }>(`/api/management/users/${id}/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }), listApiKeys: () => request('/api/management/api-keys'), createApiKey: (data: { name: string; project_id?: number }) => request<{ token: string; item: ManagedApiKey }>('/api/management/api-keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), revokeApiKey: (id: number) => request(`/api/management/api-keys/${id}`, { method: 'DELETE' }), listAuditLogs: (userId?: number) => request(`/api/management/audit-logs${userId ? `?userId=${userId}` : ''}`), listStorageConfigs: () => request('/api/management/storage-configs'), createStorageConfig: (data: { region: string; bucket: string; public_base_url: string; cdn_domain: string; path_prefix: string; secret_id: string; secret_key: string }) => request('/api/management/storage-configs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), testStorageConfig: (id: number) => request<{ success: true; test_status: 'passed'; test_message: string }>(`/api/management/storage-configs/${id}/test`, { method: 'POST' }), activateStorageConfig: (id: number) => request<{ success: true }>(`/api/management/storage-configs/${id}/activate`, { method: 'POST' }), listProjects: () => request('/api/projects'), getProject: (id: number) => request(`/api/projects/${id}`), createProject: (data: { name: string; slug: string; client_description: string; groupId?: number }) => request('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), updateProject: (id: number, data: { name: string; client_description: string }) => request(`/api/projects/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), updateCustomerAccess: (id: number, data: { enabled: boolean; password?: string; expires_at?: string | null }) => request(`/api/projects/${id}/customer-access`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), listNotes: (query: NoteListQuery = {}) => { const params = new URLSearchParams(); Object.entries(query).forEach(([key, value]) => value != null && params.set(key, String(value))); return request(`/api/notes?${params}`); }, listProjectWorks: (projectId: number, query: NoteListQuery = {}) => { const params = new URLSearchParams(); Object.entries(query).forEach(([key, value]) => value != null && params.set(key, String(value))); return request(`/api/projects/${projectId}/works?${params}`); }, getWork: (id: number, round?: number) => request(`/api/works/${id}${round ? `?round=${round}` : ''}`), getWorkFeedback: (id: number) => request(`/api/works/${id}/annotations`), createWork: (projectId: number, payload: { title: string; description: string; tags: string[]; images: File[] }) => { const form = new FormData(); form.append('title', payload.title); form.append('description', payload.description); form.append('tags', payload.tags.join(',')); payload.images.forEach((file) => form.append('images', file)); return request(`/api/projects/${projectId}/works`, { method: 'POST', body: form }); }, createRound: (workId: number, payload: { title: string; description: string; tags: string[]; images: File[] }) => { const form = new FormData(); form.append('title', payload.title); form.append('description', payload.description); form.append('tags', payload.tags.join(',')); payload.images.forEach((file) => form.append('images', file)); return request(`/api/works/${workId}/rounds`, { method: 'POST', body: form }); }, setReviewStatus: (id: number, status: ReviewStatus) => request<{ success: true; status: ReviewStatus }>(`/api/notes/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }), reopenWork: (id: number, reason: string) => request<{ success: true; status: ReviewStatus }>(`/api/notes/${id}/reopen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason }) }), addAnnotation: (imageId: number, data: { x: number; y: number; content: string; author_name?: string }) => request(`/api/images/${imageId}/annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), addTextSelectionAnnotation: (workId: number, data: { round_number: number; target: 'title' | 'description' | 'tags'; start_offset: number; end_offset: number; selected_text: string; content: string }) => request(`/api/works/${workId}/text-annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), replyToFeedback: (workId: number, type: FeedbackType, feedbackId: number, content: string) => request(`/api/works/${workId}/feedback/${type}/${feedbackId}/replies`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }), withdrawFeedback: (workId: number, type: FeedbackType, feedbackId: number) => request<{ success: true }>(`/api/works/${workId}/feedback/${type}/${feedbackId}/withdraw`, { method: 'POST' }), deleteAnnotation: (id: number) => request(`/api/annotations/${id}`, { method: 'DELETE' }), addComment: (noteId: number, data: { content: string; author_name: string; author_role?: 'client' | 'operator' }) => request(`/api/notes/${noteId}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), getCustomerAccess: (slug: string) => request(`/api/review/${slug}/access`), customerLogin: (slug: string, data: { reviewer_name: string; password: string }) => request<{ success: true; reviewer_name: string }>(`/api/review/${slug}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), getCustomerProject: (slug: string) => request<{ project: Pick; works: Note[]; reviewer_name: string }>(`/api/review/${slug}/project`), getCustomerWorkRound: (slug: string, workId: number, round?: number) => request(`/api/review/${slug}/works/${workId}${round ? `?round=${round}` : ''}`), getCustomerWorkFeedback: (slug: string, workId: number) => request(`/api/review/${slug}/works/${workId}/annotations`), addCustomerComment: (slug: string, noteId: number, content: string) => request(`/api/review/${slug}/works/${noteId}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }), addCustomerAnnotation: (slug: string, imageId: number, data: { x: number; y: number; content: string }) => request(`/api/review/${slug}/images/${imageId}/annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), addCustomerTextSelectionAnnotation: (slug: string, workId: number, data: { round_number: number; target: 'title' | 'description' | 'tags'; start_offset: number; end_offset: number; selected_text: string; content: string }) => request(`/api/review/${slug}/works/${workId}/text-annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), replyToCustomerFeedback: (slug: string, workId: number, type: FeedbackType, feedbackId: number, content: string) => request(`/api/review/${slug}/works/${workId}/feedback/${type}/${feedbackId}/replies`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }), withdrawCustomerFeedback: (slug: string, workId: number, type: FeedbackType, feedbackId: number) => request<{ success: true }>(`/api/review/${slug}/works/${workId}/feedback/${type}/${feedbackId}/withdraw`, { method: 'POST' }), submitCustomerRoundDecision: (slug: string, workId: number, roundNumber: number, decision: 'approved' | 'changes_requested', reason?: string) => request<{ success: true; status: ReviewStatus; version_number: number }>(`/api/review/${slug}/works/${workId}/decision`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ round_number: roundNumber, decision, reason }) }), };