203 lines
6.3 KiB
TypeScript
203 lines
6.3 KiB
TypeScript
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<ApiSession> {
|
|
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<Project[]>('/api/v1/projects', { token: session.token })
|
|
}
|
|
|
|
export async function fetchWorkspace(session: ApiSession, projectId: string) {
|
|
useSessionBase(session)
|
|
return request<Workspace>(`/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<Project>('/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<Expert[]>('/api/v1/ai-experts', { token: session.token })
|
|
}
|
|
|
|
export async function fetchAISessions(session: ApiSession, projectId: string) {
|
|
useSessionBase(session)
|
|
return request<AISession[]>(`/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<AISession>(`/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<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
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<T>
|
|
}
|
|
|
|
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, '')
|
|
}
|