nanobot/webui/src/lib/api.ts
2026-06-02 18:47:34 +08:00

480 lines
13 KiB
TypeScript

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<T>(
url: string,
token: string,
init?: RequestInit,
timeoutMs: number = 0,
): Promise<T> {
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("<!doctype");
throw new ApiError(
res.status,
isHtml
? "Gateway returned WebUI HTML instead of JSON. Restart nanobot gateway and try again."
: "Gateway returned a non-JSON response.",
);
}
return (await res.json()) as T;
}
function mcpValuesHeader(values: Record<string, unknown>): HeadersInit | undefined {
const payload: Record<string, unknown> = {};
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<ChatSummary[]> {
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<WebuiThreadPersistedPayload | null> {
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<boolean> {
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<SettingsPayload> {
return request<SettingsPayload>(
`${base}/api/settings`,
token,
undefined,
API_READ_TIMEOUT_MS,
);
}
export async function fetchWorkspaces(
token: string,
base: string = "",
): Promise<WorkspacesPayload> {
return request<WorkspacesPayload>(
`${base}/api/workspaces`,
token,
undefined,
API_READ_TIMEOUT_MS,
);
}
export async function fetchCliApps(
token: string,
base: string = "",
): Promise<CliAppsPayload> {
return request<CliAppsPayload>(
`${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<CliAppsPayload> {
const query = new URLSearchParams();
query.set("name", name);
return request<CliAppsPayload>(`${base}/api/settings/cli-apps/${action}?${query}`, token);
}
export async function fetchMcpPresets(
token: string,
base: string = "",
): Promise<McpPresetsPayload> {
return request<McpPresetsPayload>(
`${base}/api/settings/mcp-presets`,
token,
undefined,
API_READ_TIMEOUT_MS,
);
}
export async function fetchProviderModels(
token: string,
provider: string,
base: string = "",
): Promise<ProviderModelsPayload> {
const query = new URLSearchParams();
query.set("provider", provider);
return request<ProviderModelsPayload>(
`${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<string, string> = {},
base: string = "",
): Promise<McpPresetsPayload> {
const query = new URLSearchParams();
query.set("name", name);
return request<McpPresetsPayload>(
`${base}/api/settings/mcp-presets/${action}?${query}`,
token,
{ headers: mcpValuesHeader(values) },
);
}
export async function saveCustomMcpServer(
token: string,
values: Record<string, string>,
base: string = "",
): Promise<McpPresetsPayload> {
return request<McpPresetsPayload>(
`${base}/api/settings/mcp-presets/custom`,
token,
{ headers: mcpValuesHeader(values) },
);
}
export async function importMcpConfig(
token: string,
config: string,
base: string = "",
): Promise<McpPresetsPayload> {
return request<McpPresetsPayload>(
`${base}/api/settings/mcp-presets/import`,
token,
{ headers: mcpValuesHeader({ config }) },
);
}
export async function updateMcpServerTools(
token: string,
name: string,
enabledTools: string[],
base: string = "",
): Promise<McpPresetsPayload> {
return request<McpPresetsPayload>(
`${base}/api/settings/mcp-presets/tools`,
token,
{ headers: mcpValuesHeader({ name, enabled_tools: enabledTools }) },
);
}
export async function listSlashCommands(
token: string,
base: string = "",
): Promise<SlashCommand[]> {
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<SidebarStatePayload> {
return request<SidebarStatePayload>(
`${base}/api/webui/sidebar-state`,
token,
undefined,
API_READ_TIMEOUT_MS,
);
}
export async function updateSidebarState(
token: string,
state: SidebarStatePayload,
base: string = "",
): Promise<SidebarStatePayload> {
const query = new URLSearchParams();
query.set("state", JSON.stringify(state));
return request<SidebarStatePayload>(
`${base}/api/webui/sidebar-state/update?${query}`,
token,
);
}
export async function updateSettings(
token: string,
update: SettingsUpdate,
base: string = "",
): Promise<SettingsPayload> {
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<SettingsPayload>(`${base}/api/settings/update?${query}`, token);
}
export async function createModelConfiguration(
token: string,
configuration: ModelConfigurationCreate,
base: string = "",
): Promise<SettingsPayload> {
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<SettingsPayload>(
`${base}/api/settings/model-configurations/create?${query}`,
token,
);
}
export async function updateModelConfiguration(
token: string,
configuration: ModelConfigurationUpdate,
base: string = "",
): Promise<SettingsPayload> {
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<SettingsPayload>(
`${base}/api/settings/model-configurations/update?${query}`,
token,
);
}
export async function updateProviderSettings(
token: string,
update: ProviderSettingsUpdate,
base: string = "",
): Promise<SettingsPayload> {
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<SettingsPayload>(
`${base}/api/settings/provider/update?${query}`,
token,
);
}
export async function loginProviderOAuth(
token: string,
provider: string,
base: string = "",
): Promise<SettingsPayload> {
const query = new URLSearchParams();
query.set("provider", provider);
return request<SettingsPayload>(
`${base}/api/settings/provider/oauth-login?${query}`,
token,
);
}
export async function logoutProviderOAuth(
token: string,
provider: string,
base: string = "",
): Promise<SettingsPayload> {
const query = new URLSearchParams();
query.set("provider", provider);
return request<SettingsPayload>(
`${base}/api/settings/provider/oauth-logout?${query}`,
token,
);
}
export async function updateWebSearchSettings(
token: string,
update: WebSearchSettingsUpdate,
base: string = "",
): Promise<SettingsPayload> {
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<SettingsPayload>(
`${base}/api/settings/web-search/update?${query}`,
token,
);
}
export async function updateNetworkSafetySettings(
token: string,
update: NetworkSafetySettingsUpdate,
base: string = "",
): Promise<SettingsPayload> {
const query = new URLSearchParams();
query.set("webui_allow_local_service_access", String(update.webuiAllowLocalServiceAccess));
query.set("webui_default_access_mode", update.webuiDefaultAccessMode);
return request<SettingsPayload>(
`${base}/api/settings/network-safety/update?${query}`,
token,
);
}
export async function updateImageGenerationSettings(
token: string,
update: ImageGenerationSettingsUpdate,
base: string = "",
): Promise<SettingsPayload> {
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<SettingsPayload>(
`${base}/api/settings/image-generation/update?${query}`,
token,
);
}