mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-06 06:41:56 +00:00
Enable the web client to run in plain browser environments alongside Telegram Mini App support: - Server: Add CLI_API_TOKEN authentication path as alternative to Telegram initData - Client: Add useAuthSource hook to detect and manage Telegram vs browser auth sources - Client: Add usePlatform hook for platform abstraction with graceful haptic feedback degradation - Client: Add LoginPrompt component for browser access token login - Client: Extend useTheme to fall back to system prefers-color-scheme in browser - Client: Migrate all direct HapticFeedback calls to use usePlatform hook
144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
import type {
|
|
AuthResponse,
|
|
MachinesResponse,
|
|
MessagesResponse,
|
|
SpawnResponse,
|
|
SessionResponse,
|
|
SessionsResponse
|
|
} from '@/types/api'
|
|
|
|
export class ApiClient {
|
|
private token: string
|
|
|
|
constructor(token: string) {
|
|
this.token = token
|
|
}
|
|
|
|
private async request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const headers = new Headers(init?.headers)
|
|
headers.set('authorization', `Bearer ${this.token}`)
|
|
if (init?.body !== undefined && !headers.has('content-type')) {
|
|
headers.set('content-type', 'application/json')
|
|
}
|
|
|
|
const res = await fetch(path, {
|
|
...init,
|
|
headers
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '')
|
|
throw new Error(`HTTP ${res.status} ${res.statusText}: ${body}`)
|
|
}
|
|
|
|
return await res.json() as T
|
|
}
|
|
|
|
async authenticate(auth: { initData: string } | { accessToken: string }): Promise<AuthResponse> {
|
|
const res = await fetch('/api/auth', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(auth)
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '')
|
|
throw new Error(`Auth failed: HTTP ${res.status} ${res.statusText}: ${body}`)
|
|
}
|
|
|
|
return await res.json() as AuthResponse
|
|
}
|
|
|
|
async getSessions(): Promise<SessionsResponse> {
|
|
return await this.request<SessionsResponse>('/api/sessions')
|
|
}
|
|
|
|
async getSession(sessionId: string): Promise<SessionResponse> {
|
|
return await this.request<SessionResponse>(`/api/sessions/${encodeURIComponent(sessionId)}`)
|
|
}
|
|
|
|
async getMessages(sessionId: string, options: { beforeSeq?: number | null; limit?: number }): Promise<MessagesResponse> {
|
|
const params = new URLSearchParams()
|
|
if (options.beforeSeq !== undefined && options.beforeSeq !== null) {
|
|
params.set('beforeSeq', `${options.beforeSeq}`)
|
|
}
|
|
if (options.limit !== undefined && options.limit !== null) {
|
|
params.set('limit', `${options.limit}`)
|
|
}
|
|
|
|
const qs = params.toString()
|
|
const url = `/api/sessions/${encodeURIComponent(sessionId)}/messages${qs ? `?${qs}` : ''}`
|
|
return await this.request<MessagesResponse>(url)
|
|
}
|
|
|
|
async sendMessage(sessionId: string, text: string, localId?: string | null): Promise<void> {
|
|
await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ text, localId: localId ?? undefined })
|
|
})
|
|
}
|
|
|
|
async abortSession(sessionId: string): Promise<void> {
|
|
await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/abort`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({})
|
|
})
|
|
}
|
|
|
|
async setPermissionMode(sessionId: string, mode: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'): Promise<void> {
|
|
await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/permission-mode`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ mode })
|
|
})
|
|
}
|
|
|
|
async setModelMode(sessionId: string, model: 'default' | 'sonnet' | 'opus'): Promise<void> {
|
|
await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/model`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ model })
|
|
})
|
|
}
|
|
|
|
async approvePermission(
|
|
sessionId: string,
|
|
requestId: string,
|
|
modeOrOptions?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | {
|
|
mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
|
|
allowTools?: string[]
|
|
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'
|
|
}
|
|
): Promise<void> {
|
|
const body = typeof modeOrOptions === 'string' || modeOrOptions === undefined
|
|
? { mode: modeOrOptions }
|
|
: modeOrOptions
|
|
await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(requestId)}/approve`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(body)
|
|
})
|
|
}
|
|
|
|
async denyPermission(
|
|
sessionId: string,
|
|
requestId: string,
|
|
options?: {
|
|
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'
|
|
}
|
|
): Promise<void> {
|
|
await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(requestId)}/deny`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(options ?? {})
|
|
})
|
|
}
|
|
|
|
async getMachines(): Promise<MachinesResponse> {
|
|
return await this.request<MachinesResponse>('/api/machines')
|
|
}
|
|
|
|
async spawnSession(machineId: string, directory: string, agent?: 'claude' | 'codex'): Promise<SpawnResponse> {
|
|
return await this.request<SpawnResponse>(`/api/machines/${encodeURIComponent(machineId)}/spawn`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ directory, agent })
|
|
})
|
|
}
|
|
}
|