mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-14 06:43:53 +00:00
480 lines
13 KiB
TypeScript
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,
|
|
);
|
|
}
|