import type { Annotation, AuditLogEntry, CurrentUser, CustomerAccessState, ManagedApiKey, ManagedUser, Note, NoteDetail, NoteListQuery, OperationGroup, Project, ReviewStatus, StorageConfig, TextAnnotation, UserRole, WorkCollection, WorkComment } 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) }), listCollections: (projectId: number) => request(`/api/projects/${projectId}/collections`), createCollection: (projectId: number, data: { name: string; client_description: string }) => request(`/api/projects/${projectId}/collections`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), updateCollection: (projectId: number, id: number, data: { name: string; client_description: string }) => request(`/api/projects/${projectId}/collections/${id}`, { 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}`); }, getNote: (id: number, version?: number) => request(`/api/notes/${id}${version ? `?version=${version}` : ''}`), createNote: (payload: { collectionId: number; title: string; description: string; tags: string[]; images: File[] }) => { const form = new FormData(); form.append('collectionId', String(payload.collectionId)); 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/notes', { method: 'POST', body: form }); }, createWorkVersion: (noteId: 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/notes/${noteId}/versions`, { 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) }), addTextAnnotation: (noteId: number, data: { version_number: number; target: 'title' | 'description'; content: string }) => request(`/api/notes/${noteId}/text-annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), 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; collections: WorkCollection[]; reviewer_name: string }>(`/api/review/${slug}/project`), getCustomerCollection: (slug: string, collectionId: number) => request<{ collection: WorkCollection; works: Note[] }>(`/api/review/${slug}/collections/${collectionId}/works`), getCustomerWork: (slug: string, noteId: number, version?: number) => request(`/api/review/${slug}/works/${noteId}${version ? `?version=${version}` : ''}`), 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) }), addCustomerTextAnnotation: (slug: string, noteId: number, data: { version_number: number; target: 'title' | 'description'; content: string }) => request(`/api/review/${slug}/works/${noteId}/text-annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }), submitCustomerDecision: (slug: string, noteId: number, decision: 'approved' | 'changes_requested', reason?: string) => request<{ success: true; status: ReviewStatus }>(`/api/review/${slug}/works/${noteId}/decision`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ decision, reason }) }), };