226 lines
7.7 KiB
TypeScript
226 lines
7.7 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
import { arch, freemem, hostname, platform, release, totalmem, type as osType } from "node:os";
|
|
import { readWebuiAvatarSettings, writeWebuiAvatarSettings } from "../db/index.ts";
|
|
import { setExtensionEnabled } from "../settings/extension-settings.ts";
|
|
import { listMcpSettings, setMcpServerEnabled, setMcpToolEnabled } from "../settings/mcp-settings.ts";
|
|
import { listSkillSettings, setSkillEnabled } from "../settings/skills-settings.ts";
|
|
import { normalizeAvatarUrl } from "../services/avatars.ts";
|
|
import {
|
|
listExtensionsForSettings,
|
|
listLoadedExtensionsByPath,
|
|
mergeExtensionToggleResponse,
|
|
} from "../services/extensions-display.ts";
|
|
import { readModelsConfig, writeModelsConfig } from "../services/models-config.ts";
|
|
import { readSystemPrompt, writeSystemPrompt } from "../services/system-prompt.ts";
|
|
import type { WebUiContext } from "../types/context.ts";
|
|
import { json, readBody } from "../http/request.ts";
|
|
|
|
function listMcpTools(ctx: WebUiContext): Record<string, unknown>[] {
|
|
return listMcpSettings(ctx.config.paths.mcpConfigFile, ctx.config.paths.mcpCacheFile);
|
|
}
|
|
|
|
export function handleSettingsRoute(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
ctx: WebUiContext,
|
|
pathname: string,
|
|
): boolean {
|
|
const { paths } = ctx.config;
|
|
const { sendCmd } = ctx.rpc;
|
|
|
|
if (req.method === "GET" && pathname === "/api/settings") {
|
|
void listSkillSettings(paths.repoRoot, paths.agentDir)
|
|
.then((skills) =>
|
|
listExtensionsForSettings(paths, sendCmd).then((extensions) =>
|
|
json(res, {
|
|
...readWebuiAvatarSettings(),
|
|
webuiDbPath: ctx.db.dbPath,
|
|
systemPrompt: readSystemPrompt(paths),
|
|
systemPromptPath: paths.systemPromptFile,
|
|
modelsConfig: readModelsConfig(paths),
|
|
modelsConfigPath: paths.modelsConfigFile,
|
|
extensionsPath: paths.agentExtensionsDir,
|
|
mcpCachePath: paths.mcpCacheFile,
|
|
mcpConfigPath: paths.mcpConfigFile,
|
|
skills,
|
|
extensions,
|
|
mcpTools: listMcpTools(ctx),
|
|
}),
|
|
),
|
|
)
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "POST" && pathname === "/api/settings/skills/toggle") {
|
|
void readBody(req)
|
|
.then(async ({ path: skillPath, enabled }) => {
|
|
if (typeof skillPath !== "string" || !skillPath.trim()) {
|
|
throw new Error("skill path 无效");
|
|
}
|
|
if (typeof enabled !== "boolean") {
|
|
throw new Error("enabled 必须是 boolean");
|
|
}
|
|
const skill = await setSkillEnabled(paths.repoRoot, paths.agentDir, skillPath.trim(), enabled);
|
|
const reload = await sendCmd({ type: "reload" });
|
|
if (!reload.success) throw new Error(reload.error || "Agent 重新加载失败");
|
|
json(res, { ok: true, skill });
|
|
})
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "POST" && pathname === "/api/settings/extensions/toggle") {
|
|
void readBody(req)
|
|
.then(async ({ path: extensionPath, enabled }) => {
|
|
if (typeof extensionPath !== "string" || !extensionPath.trim()) {
|
|
throw new Error("extension path 无效");
|
|
}
|
|
if (typeof enabled !== "boolean") {
|
|
throw new Error("enabled 必须是 boolean");
|
|
}
|
|
const extension = await setExtensionEnabled(
|
|
paths.repoRoot,
|
|
paths.agentDir,
|
|
extensionPath.trim(),
|
|
enabled,
|
|
);
|
|
const reload = await sendCmd({ type: "reload" });
|
|
if (!reload.success) throw new Error(reload.error || "Agent 重新加载失败");
|
|
let loadedByPath = new Map<string, any>();
|
|
try {
|
|
loadedByPath = await listLoadedExtensionsByPath(paths, sendCmd);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
json(res, {
|
|
ok: true,
|
|
extension: mergeExtensionToggleResponse(paths, extension, loadedByPath),
|
|
});
|
|
})
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "POST" && pathname === "/api/settings/mcp/server/toggle") {
|
|
void readBody(req)
|
|
.then(async ({ server, enabled }) => {
|
|
if (typeof server !== "string" || !server.trim()) {
|
|
throw new Error("server 名称无效");
|
|
}
|
|
if (typeof enabled !== "boolean") {
|
|
throw new Error("enabled 必须是 boolean");
|
|
}
|
|
const entry = setMcpServerEnabled(
|
|
paths.mcpConfigFile,
|
|
paths.mcpCacheFile,
|
|
server.trim(),
|
|
enabled,
|
|
);
|
|
const reload = await sendCmd({ type: "reload" });
|
|
if (!reload.success) throw new Error(reload.error || "Agent 重新加载失败");
|
|
json(res, { ok: true, server: entry });
|
|
})
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "POST" && pathname === "/api/settings/mcp/tool/toggle") {
|
|
void readBody(req)
|
|
.then(async ({ server, tool, enabled }) => {
|
|
if (typeof server !== "string" || !server.trim()) {
|
|
throw new Error("server 名称无效");
|
|
}
|
|
if (typeof tool !== "string" || !tool.trim()) {
|
|
throw new Error("tool 名称无效");
|
|
}
|
|
if (typeof enabled !== "boolean") {
|
|
throw new Error("enabled 必须是 boolean");
|
|
}
|
|
const entry = setMcpToolEnabled(
|
|
paths.mcpConfigFile,
|
|
paths.mcpCacheFile,
|
|
server.trim(),
|
|
tool.trim(),
|
|
enabled,
|
|
);
|
|
const reload = await sendCmd({ type: "reload" });
|
|
if (!reload.success) throw new Error(reload.error || "Agent 重新加载失败");
|
|
json(res, { ok: true, tool: entry });
|
|
})
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "POST" && pathname === "/api/settings/reload") {
|
|
void sendCmd({ type: "reload" })
|
|
.then((r: any) => (r.success ? json(res, { ok: true }) : json(res, { error: r.error }, 500)))
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "POST" && pathname === "/api/settings/models-config") {
|
|
void readBody(req)
|
|
.then(async ({ modelsConfig }) => {
|
|
if (typeof modelsConfig !== "string") {
|
|
throw new Error("modelsConfig 必须是字符串");
|
|
}
|
|
writeModelsConfig(paths, modelsConfig);
|
|
const reload = await sendCmd({ type: "reload" });
|
|
if (!reload.success) throw new Error(reload.error || "Agent 重新加载失败");
|
|
json(res, { ok: true, modelsConfigPath: paths.modelsConfigFile });
|
|
})
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "POST" && pathname === "/api/settings/system-prompt") {
|
|
void readBody(req)
|
|
.then(async ({ systemPrompt }) => {
|
|
if (typeof systemPrompt !== "string") {
|
|
throw new Error("systemPrompt 必须是字符串");
|
|
}
|
|
writeSystemPrompt(paths, systemPrompt);
|
|
const reload = await sendCmd({ type: "reload" });
|
|
if (!reload.success) throw new Error(reload.error || "Agent 重新加载失败");
|
|
json(res, { ok: true, systemPromptPath: paths.systemPromptFile });
|
|
})
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "POST" && pathname === "/api/settings/avatars") {
|
|
void readBody(req)
|
|
.then(({ userAvatarUrl, agentAvatarUrl }) => {
|
|
const settings = {
|
|
userAvatarUrl: normalizeAvatarUrl(userAvatarUrl),
|
|
agentAvatarUrl: normalizeAvatarUrl(agentAvatarUrl),
|
|
};
|
|
writeWebuiAvatarSettings(settings);
|
|
json(res, { ok: true, ...settings });
|
|
})
|
|
.catch((err) => json(res, { error: err.message }, 500));
|
|
return true;
|
|
}
|
|
|
|
if (req.method === "GET" && pathname === "/api/environment") {
|
|
json(res, {
|
|
nodeVersion: process.version,
|
|
platform: platform(),
|
|
arch: arch(),
|
|
osType: osType(),
|
|
osRelease: release(),
|
|
hostname: hostname(),
|
|
pid: process.pid,
|
|
cwd: process.cwd(),
|
|
uptime: Math.floor(process.uptime()),
|
|
totalMemMb: Math.round(totalmem() / 1024 / 1024),
|
|
freeMemMb: Math.round(freemem() / 1024 / 1024),
|
|
execPath: process.execPath,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|