mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
207 lines
5.7 KiB
TypeScript
207 lines
5.7 KiB
TypeScript
const ENV_BASE_URL =
|
|
typeof import.meta !== 'undefined' &&
|
|
typeof import.meta.env?.VITE_DESKTOP_SERVER_URL === 'string' &&
|
|
import.meta.env.VITE_DESKTOP_SERVER_URL.length > 0
|
|
? import.meta.env.VITE_DESKTOP_SERVER_URL
|
|
: undefined
|
|
|
|
const DEFAULT_BASE_URL = ENV_BASE_URL || 'http://127.0.0.1:3456'
|
|
|
|
let baseUrl = DEFAULT_BASE_URL
|
|
let authToken: string | null = null
|
|
const DIAGNOSTICS_PATH = '/api/diagnostics/events'
|
|
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000
|
|
const DIAGNOSTICS_REQUEST_TIMEOUT_MS = 5_000
|
|
|
|
function getErrorMessage(status: number, body: unknown) {
|
|
if (body && typeof body === 'object' && 'message' in body && typeof body.message === 'string') {
|
|
return body.message
|
|
}
|
|
|
|
if (typeof body === 'string' && body.trim().length > 0) {
|
|
return body
|
|
}
|
|
|
|
return `API error ${status}`
|
|
}
|
|
|
|
export function setBaseUrl(url: string) {
|
|
baseUrl = url.replace(/\/$/, '')
|
|
}
|
|
|
|
export function getBaseUrl() {
|
|
return baseUrl
|
|
}
|
|
|
|
export function getApiUrl(pathOrUrl: string) {
|
|
try {
|
|
return new URL(pathOrUrl).toString()
|
|
} catch {
|
|
const normalizedPath = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`
|
|
return `${baseUrl}${normalizedPath}`
|
|
}
|
|
}
|
|
|
|
export function setAuthToken(token: string | null) {
|
|
const trimmed = token?.trim() ?? ''
|
|
authToken = trimmed.length > 0 ? trimmed : null
|
|
}
|
|
|
|
export function getAuthToken() {
|
|
return authToken
|
|
}
|
|
|
|
export function getDefaultBaseUrl() {
|
|
return DEFAULT_BASE_URL
|
|
}
|
|
|
|
export function hasExplicitDefaultBaseUrl() {
|
|
return Boolean(ENV_BASE_URL)
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public body: unknown,
|
|
) {
|
|
super(getErrorMessage(status, body))
|
|
this.name = 'ApiError'
|
|
}
|
|
}
|
|
|
|
export type ApiRequestOptions = {
|
|
timeout?: number
|
|
signal?: AbortSignal
|
|
}
|
|
|
|
async function request<T>(method: string, path: string, body?: unknown, options?: ApiRequestOptions): Promise<T> {
|
|
const url = `${baseUrl}${path}`
|
|
const headers = buildHeaders()
|
|
|
|
const controller = new AbortController()
|
|
const timeoutMs = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
let timedOut = false
|
|
const timeout = setTimeout(() => {
|
|
timedOut = true
|
|
controller.abort()
|
|
}, timeoutMs)
|
|
const abortFromCaller = () => controller.abort(options?.signal?.reason)
|
|
if (options?.signal?.aborted) abortFromCaller()
|
|
else options?.signal?.addEventListener('abort', abortFromCaller, { once: true })
|
|
try {
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers,
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
signal: controller.signal,
|
|
})
|
|
if (!res.ok) {
|
|
const errorBody = await res.json().catch(() => res.text())
|
|
throw new ApiError(res.status, errorBody)
|
|
}
|
|
|
|
if (res.status === 204) return undefined as T
|
|
return await res.json() as T
|
|
} catch (err) {
|
|
if (timedOut) {
|
|
const timeoutError = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`)
|
|
reportApiFailure(method, path, timeoutError)
|
|
throw timeoutError
|
|
}
|
|
if (options?.signal?.aborted) {
|
|
throw options.signal.reason instanceof Error
|
|
? options.signal.reason
|
|
: new DOMException('The operation was aborted', 'AbortError')
|
|
}
|
|
reportApiFailure(method, path, err)
|
|
throw err
|
|
} finally {
|
|
clearTimeout(timeout)
|
|
options?.signal?.removeEventListener('abort', abortFromCaller)
|
|
}
|
|
}
|
|
|
|
function reportApiFailure(method: string, path: string, error: unknown) {
|
|
if (path.startsWith('/api/diagnostics')) return
|
|
|
|
const details: Record<string, unknown> = {
|
|
method,
|
|
path,
|
|
errorName: error instanceof Error ? error.name : typeof error,
|
|
message: sanitizeDiagnosticValue(error instanceof Error ? error.message : String(error)),
|
|
}
|
|
|
|
if (error instanceof ApiError) {
|
|
details.status = error.status
|
|
details.response = sanitizeDiagnosticValue(error.body)
|
|
}
|
|
|
|
void rawRecordDiagnosticEvent({
|
|
type: 'client_api_request_failed',
|
|
severity: 'warn',
|
|
summary: `${method} ${path} failed: ${details.message}`,
|
|
details,
|
|
})
|
|
}
|
|
|
|
export function rawRecordDiagnosticEvent(event: {
|
|
type: string
|
|
severity?: 'debug' | 'info' | 'warn' | 'error'
|
|
summary: string
|
|
sessionId?: string
|
|
details?: unknown
|
|
}) {
|
|
const controller = new AbortController()
|
|
const timeout = setTimeout(() => controller.abort(), DIAGNOSTICS_REQUEST_TIMEOUT_MS)
|
|
return fetch(`${baseUrl}${DIAGNOSTICS_PATH}`, {
|
|
method: 'POST',
|
|
headers: buildHeaders(),
|
|
body: JSON.stringify(event),
|
|
signal: controller.signal,
|
|
}).then(async response => {
|
|
await response.arrayBuffer()
|
|
})
|
|
.catch(() => undefined)
|
|
.finally(() => clearTimeout(timeout))
|
|
}
|
|
|
|
function buildHeaders(): Record<string, string> {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
|
|
if (authToken) {
|
|
headers.Authorization = `Bearer ${authToken}`
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
function sanitizeDiagnosticValue(value: unknown): unknown {
|
|
if (!authToken) return value
|
|
|
|
if (typeof value === 'string') {
|
|
return value.split(authToken).join('[redacted]')
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.map((entry) => sanitizeDiagnosticValue(entry))
|
|
}
|
|
|
|
if (value && typeof value === 'object') {
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([key, entry]) => [key, sanitizeDiagnosticValue(entry)]),
|
|
)
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string, options?: ApiRequestOptions) => request<T>('GET', path, undefined, options),
|
|
post: <T>(path: string, body?: unknown, options?: ApiRequestOptions) => request<T>('POST', path, body, options),
|
|
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
|
|
patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body),
|
|
delete: <T>(path: string) => request<T>('DELETE', path),
|
|
}
|