import type { ChatSummary, CliAppsPayload, ImageGenerationSettingsUpdate, McpPresetsPayload, ModelConfigurationCreate, ModelConfigurationUpdate, NetworkSafetySettingsUpdate, ProviderModelsPayload, ProviderSettingsUpdate, SettingsPayload, SettingsUpdate, SidebarStatePayload, SlashCommand, WebSearchSettingsUpdate, WorkspacesPayload, WebuiThreadPersistedPayload, WorkspaceScopePayload, } from "./types"; import { fetchWithTimeout } from "./http"; const API_READ_TIMEOUT_MS = 20_000; export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.status = status; this.name = "ApiError"; } } async function request( url: string, token: string, init?: RequestInit, timeoutMs: number = 0, ): Promise { const res = await fetchWithTimeout( url, { ...(init ?? {}), headers: { ...(init?.headers ?? {}), Authorization: `Bearer ${token}`, }, credentials: "same-origin", }, timeoutMs, ); if (!res.ok) { const text = typeof res.text === "function" ? (await res.text()).trim() : ""; throw new ApiError(res.status, text || `HTTP ${res.status}`); } const contentType = res.headers?.get?.("content-type") ?? ""; if (contentType && !contentType.toLowerCase().includes("application/json")) { const text = typeof res.text === "function" ? await res.text() : ""; const isHtml = text.trimStart().toLowerCase().startsWith("): HeadersInit | undefined { const payload: Record = {}; Object.entries(values).forEach(([key, value]) => { if (value === null || value === undefined) return; if (typeof value === "string") { const trimmed = value.trim(); if (trimmed) payload[key] = trimmed; return; } payload[key] = value; }); if (!Object.keys(payload).length) return undefined; return { "X-Nanobot-MCP-Values": JSON.stringify(payload) }; } function splitKey(key: string): { channel: string; chatId: string } { const idx = key.indexOf(":"); if (idx === -1) return { channel: "", chatId: key }; return { channel: key.slice(0, idx), chatId: key.slice(idx + 1) }; } export async function listSessions( token: string, base: string = "", ): Promise { type Row = { key: string; created_at: string | null; updated_at: string | null; title?: string; preview?: string; run_started_at?: number | null; workspace_scope?: WorkspaceScopePayload | null; }; const body = await request<{ sessions: Row[] }>( `${base}/api/sessions`, token, undefined, API_READ_TIMEOUT_MS, ); return body.sessions.map((s) => ({ key: s.key, ...splitKey(s.key), createdAt: s.created_at, updatedAt: s.updated_at, title: s.title ?? "", preview: s.preview ?? "", runStartedAt: s.run_started_at ?? null, workspaceScope: s.workspace_scope ?? null, })); } /** Disk-backed WebUI display thread snapshot (separate from agent session). */ export async function fetchWebuiThread( token: string, key: string, base: string = "", ): Promise { const url = `${base}/api/sessions/${encodeURIComponent(key)}/webui-thread`; const res = await fetchWithTimeout(url, { headers: { Authorization: `Bearer ${token}` }, credentials: "same-origin", }); if (res.status === 404) return null; if (!res.ok) throw new ApiError(res.status, `HTTP ${res.status}`); return (await res.json()) as WebuiThreadPersistedPayload; } export async function deleteSession( token: string, key: string, base: string = "", ): Promise { const body = await request<{ deleted: boolean }>( `${base}/api/sessions/${encodeURIComponent(key)}/delete`, token, ); return body.deleted; } export async function fetchSettings( token: string, base: string = "", ): Promise { return request( `${base}/api/settings`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchWorkspaces( token: string, base: string = "", ): Promise { return request( `${base}/api/workspaces`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchCliApps( token: string, base: string = "", ): Promise { return request( `${base}/api/settings/cli-apps`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function runCliAppAction( token: string, action: "install" | "update" | "uninstall" | "test", name: string, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("name", name); return request(`${base}/api/settings/cli-apps/${action}?${query}`, token); } export async function fetchMcpPresets( token: string, base: string = "", ): Promise { return request( `${base}/api/settings/mcp-presets`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchProviderModels( token: string, provider: string, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("provider", provider); return request( `${base}/api/settings/provider-models?${query}`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function runMcpPresetAction( token: string, action: "enable" | "remove" | "test", name: string, values: Record = {}, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("name", name); return request( `${base}/api/settings/mcp-presets/${action}?${query}`, token, { headers: mcpValuesHeader(values) }, ); } export async function saveCustomMcpServer( token: string, values: Record, base: string = "", ): Promise { return request( `${base}/api/settings/mcp-presets/custom`, token, { headers: mcpValuesHeader(values) }, ); } export async function importMcpConfig( token: string, config: string, base: string = "", ): Promise { return request( `${base}/api/settings/mcp-presets/import`, token, { headers: mcpValuesHeader({ config }) }, ); } export async function updateMcpServerTools( token: string, name: string, enabledTools: string[], base: string = "", ): Promise { return request( `${base}/api/settings/mcp-presets/tools`, token, { headers: mcpValuesHeader({ name, enabled_tools: enabledTools }) }, ); } export async function listSlashCommands( token: string, base: string = "", ): Promise { type Row = { command: string; title: string; description: string; icon: string; arg_hint?: string; }; const body = await request<{ commands: Row[] }>( `${base}/api/commands`, token, undefined, API_READ_TIMEOUT_MS, ); return body.commands .filter((command) => !["/stop", "/restart"].includes(command.command)) .map((command) => ({ command: command.command, title: command.title, description: command.description, icon: command.icon, argHint: command.arg_hint ?? "", })); } export async function fetchSidebarState( token: string, base: string = "", ): Promise { return request( `${base}/api/webui/sidebar-state`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function updateSidebarState( token: string, state: SidebarStatePayload, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("state", JSON.stringify(state)); return request( `${base}/api/webui/sidebar-state/update?${query}`, token, ); } export async function updateSettings( token: string, update: SettingsUpdate, base: string = "", ): Promise { const query = new URLSearchParams(); if (update.modelPreset !== undefined) { query.set("model_preset", update.modelPreset ?? "default"); } if (update.model !== undefined) query.set("model", update.model); if (update.provider !== undefined) query.set("provider", update.provider); if (update.contextWindowTokens !== undefined) { query.set("context_window_tokens", String(update.contextWindowTokens)); } if (update.timezone !== undefined) query.set("timezone", update.timezone); if (update.botName !== undefined) query.set("bot_name", update.botName); if (update.botIcon !== undefined) query.set("bot_icon", update.botIcon); if (update.toolHintMaxLength !== undefined) { query.set("tool_hint_max_length", String(update.toolHintMaxLength)); } return request(`${base}/api/settings/update?${query}`, token); } export async function createModelConfiguration( token: string, configuration: ModelConfigurationCreate, base: string = "", ): Promise { const query = new URLSearchParams(); if (configuration.name !== undefined) query.set("name", configuration.name); query.set("label", configuration.label); query.set("provider", configuration.provider); query.set("model", configuration.model); return request( `${base}/api/settings/model-configurations/create?${query}`, token, ); } export async function updateModelConfiguration( token: string, configuration: ModelConfigurationUpdate, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("name", configuration.name); if (configuration.label !== undefined) query.set("label", configuration.label); if (configuration.provider !== undefined) query.set("provider", configuration.provider); if (configuration.model !== undefined) query.set("model", configuration.model); if (configuration.contextWindowTokens !== undefined) { query.set("context_window_tokens", String(configuration.contextWindowTokens)); } return request( `${base}/api/settings/model-configurations/update?${query}`, token, ); } export async function updateProviderSettings( token: string, update: ProviderSettingsUpdate, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("provider", update.provider); if (update.apiKey !== undefined) query.set("api_key", update.apiKey); if (update.apiBase !== undefined) query.set("api_base", update.apiBase); if (update.apiType !== undefined) query.set("api_type", update.apiType); return request( `${base}/api/settings/provider/update?${query}`, token, ); } export async function loginProviderOAuth( token: string, provider: string, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("provider", provider); return request( `${base}/api/settings/provider/oauth-login?${query}`, token, ); } export async function logoutProviderOAuth( token: string, provider: string, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("provider", provider); return request( `${base}/api/settings/provider/oauth-logout?${query}`, token, ); } export async function updateWebSearchSettings( token: string, update: WebSearchSettingsUpdate, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("provider", update.provider); if (update.apiKey !== undefined) query.set("api_key", update.apiKey); if (update.baseUrl !== undefined) query.set("base_url", update.baseUrl); if (update.maxResults !== undefined) query.set("max_results", String(update.maxResults)); if (update.timeout !== undefined) query.set("timeout", String(update.timeout)); if (update.useJinaReader !== undefined) { query.set("use_jina_reader", String(update.useJinaReader)); } return request( `${base}/api/settings/web-search/update?${query}`, token, ); } export async function updateNetworkSafetySettings( token: string, update: NetworkSafetySettingsUpdate, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("webui_allow_local_service_access", String(update.webuiAllowLocalServiceAccess)); query.set("webui_default_access_mode", update.webuiDefaultAccessMode); return request( `${base}/api/settings/network-safety/update?${query}`, token, ); } export async function updateImageGenerationSettings( token: string, update: ImageGenerationSettingsUpdate, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("enabled", String(update.enabled)); query.set("provider", update.provider); query.set("model", update.model); query.set("default_aspect_ratio", update.defaultAspectRatio); query.set("default_image_size", update.defaultImageSize); query.set("max_images_per_turn", String(update.maxImagesPerTurn)); return request( `${base}/api/settings/image-generation/update?${query}`, token, ); }