Files
agent/apps/mini/src/api.ts

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, '')
}