export type ApiSession = { baseUrl: string; token: string } export type Project = { id: string name: string identifier: string icon: string background: string description: string } export type WorkspaceTask = { id: string projectId: string title: string summary: string completed: boolean owner: string due: string | null createdAt: string completedAt: string | null tagId: string | null tag: string } export type NoteSource = { id: string projectId: string kind: string title: string updatedAt: string tag: string source: string } export type CronPlan = { id: string projectId: string title: string schedule: string nextRun: string | null enabled: boolean lastResult: string owner: string } export type Workspace = { project: Project & { initials: string; unreadCount: number } tags: Array<{ id: string; name: string }> tasks: WorkspaceTask[] notesSources: NoteSource[] aiSessions: Array<{ id: string; projectId: string; title: string; summary: string; updatedAt: string; references: string[] }> cronPlans: CronPlan[] } export type Expert = { id: string slug: string category: string categoryName: string name: string description: string emoji: string color: string } export type AISession = { id: string projectId: string title: string context: string status: string expert: Expert | null createdAt: string updatedAt: string } export class ApiError extends Error { constructor(public status: number, public code: string, message: string) { super(message) this.name = 'ApiError' } } let baseUrl = normalizeBaseUrl(import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:9150') export function configureApi(value: string) { baseUrl = normalizeBaseUrl(value) } export async function login(server: string, email: string, password: string): Promise { configureApi(server) const response = await request<{ token: string }>('/api/v1/auth/login', { method: 'POST', body: { email, password } }) return { baseUrl, token: response.token } } export async function fetchProjects(session: ApiSession) { useSessionBase(session) return request('/api/v1/projects', { token: session.token }) } export async function fetchWorkspace(session: ApiSession, projectId: string) { useSessionBase(session) return request(`/api/v1/projects/${projectId}/workspace`, { token: session.token }) } export async function createProject(session: ApiSession, name: string) { useSessionBase(session) const identifier = `${slugify(name) || 'project'}-${Date.now().toString(36)}` return request('/api/v1/projects', { method: 'POST', token: session.token, body: { name, identifier, icon: 'folder', background: '#165DFF', description: '' }, }) } export async function createTask(session: ApiSession, projectId: string, title: string, tag?: string) { useSessionBase(session) return request(`/api/v1/projects/${projectId}/tasks`, { method: 'POST', token: session.token, body: { title, description: '', status: 'open', tag }, }) } export async function updateTask(session: ApiSession, projectId: string, task: WorkspaceTask) { useSessionBase(session) return request(`/api/v1/projects/${projectId}/tasks/${task.id}`, { method: 'PATCH', token: session.token, body: { title: task.title, description: task.summary, completed: !task.completed, tag: task.tag }, }) } export async function uploadSource(session: ApiSession, projectId: string, file: File) { useSessionBase(session) const body = new FormData() body.append('file', file) body.append('title', file.name) return request(`/api/v1/projects/${projectId}/sources`, { method: 'POST', token: session.token, body }) } export async function createCron(session: ApiSession, projectId: string, title: string, schedule: string) { useSessionBase(session) return request(`/api/v1/projects/${projectId}/cron-plans`, { method: 'POST', token: session.token, body: { title, schedule, enabled: true }, }) } export async function fetchExperts(session: ApiSession) { useSessionBase(session) return request('/api/v1/ai-experts', { token: session.token }) } export async function fetchAISessions(session: ApiSession, projectId: string) { useSessionBase(session) return request(`/api/v1/projects/${projectId}/ai-sessions`, { token: session.token }) } export async function createAISession(session: ApiSession, projectId: string, expertId: string, context: string) { useSessionBase(session) return request(`/api/v1/projects/${projectId}/ai-sessions`, { method: 'POST', token: session.token, body: { title: Array.from(context).slice(0, 36).join(''), context, expertId }, }) } type RequestOptions = { method?: string; token?: string; body?: unknown; signal?: AbortSignal } async function request(path: string, options: RequestOptions = {}): Promise { const isForm = options.body instanceof FormData let response: Response try { response = await fetch(`${baseUrl}${path}`, { method: options.method ?? 'GET', headers: { ...(!isForm && options.body !== undefined ? { 'Content-Type': 'application/json' } : {}), ...(options.token ? { Authorization: `Bearer ${options.token}` } : {}), }, body: options.body === undefined ? undefined : isForm ? options.body as FormData : JSON.stringify(options.body), signal: options.signal, }) } catch { throw new ApiError(0, 'network_error', '无法连接服务器,请检查地址和网络') } if (!response.ok) { const payload = await response.json().catch(() => null) as { error?: { code?: string; message?: string } } | null throw new ApiError(response.status, payload?.error?.code ?? 'request_failed', payload?.error?.message ?? '请求失败,请稍后重试') } if (response.status === 204) return undefined as T return response.json() as Promise } function useSessionBase(session: ApiSession) { baseUrl = normalizeBaseUrl(session.baseUrl) } function normalizeBaseUrl(value: string) { const normalized = value.trim().replace(/\/+$/, '') if (!normalized || /^https?:\/\//i.test(normalized)) return normalized return `http://${normalized}` } function slugify(value: string) { return value.toLowerCase().trim().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-').replace(/^-|-$/g, '') }