You've already forked agentic-coding-workflow
95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
import { ApiException, type ApiError } from './types'
|
|
|
|
const BASE = '' // 同源;dev 走 vite proxy
|
|
const DEFAULT_TIMEOUT_MS = 30_000
|
|
|
|
interface RequestOptions {
|
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
body?: unknown
|
|
signal?: AbortSignal
|
|
}
|
|
|
|
let authTokenProvider: () => string | null = () => null
|
|
let onUnauthorized: () => void = () => {}
|
|
let onForbidden: () => void = () => {}
|
|
|
|
export function configure(opts: {
|
|
tokenProvider: () => string | null
|
|
onUnauthorized: () => void
|
|
onForbidden?: () => void
|
|
}) {
|
|
authTokenProvider = opts.tokenProvider
|
|
onUnauthorized = opts.onUnauthorized
|
|
if (opts.onForbidden) onForbidden = opts.onForbidden
|
|
}
|
|
|
|
export async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
|
const isForm = opts.body instanceof FormData
|
|
const headers: Record<string, string> = {
|
|
'X-Client-Request-ID': crypto.randomUUID(),
|
|
}
|
|
if (!isForm) headers['Content-Type'] = 'application/json'
|
|
const token = authTokenProvider()
|
|
if (token) headers['Authorization'] = `Bearer ${token}`
|
|
|
|
const controller = new AbortController()
|
|
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS)
|
|
if (opts.signal) {
|
|
if (opts.signal.aborted) {
|
|
controller.abort()
|
|
} else {
|
|
opts.signal.addEventListener('abort', () => controller.abort(), { once: true })
|
|
}
|
|
}
|
|
|
|
try {
|
|
const body =
|
|
opts.body === undefined
|
|
? undefined
|
|
: isForm
|
|
? (opts.body as FormData)
|
|
: JSON.stringify(opts.body)
|
|
const resp = await fetch(BASE + path, {
|
|
method: opts.method || 'GET',
|
|
headers,
|
|
body,
|
|
signal: controller.signal,
|
|
})
|
|
|
|
if (resp.status === 401) {
|
|
onUnauthorized()
|
|
} else if (resp.status === 403) {
|
|
onForbidden()
|
|
}
|
|
|
|
if (resp.status === 204) {
|
|
return undefined as T
|
|
}
|
|
|
|
const text = await resp.text()
|
|
const data = text ? JSON.parse(text) : null
|
|
|
|
if (!resp.ok) {
|
|
throw new ApiException(
|
|
resp.status,
|
|
(data as ApiError) || { code: 'unknown', message: resp.statusText },
|
|
)
|
|
}
|
|
return data as T
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
// buildQuery 把任意对象拼成 "?k=v&..." 形式;undefined / null / '' 跳过;
|
|
// 数组与对象用 String() 退化(业务侧目前没有数组 query 需求)。
|
|
export function buildQuery(params: object): string {
|
|
const q = new URLSearchParams()
|
|
for (const [k, v] of Object.entries(params)) {
|
|
if (v === undefined || v === null || v === '') continue
|
|
q.set(k, String(v))
|
|
}
|
|
const s = q.toString()
|
|
return s ? '?' + s : ''
|
|
}
|