Files
sproutclaw/.pi/agent/extensions/webui/backend/routes/settings.ts
root cf5edd6394
Some checks failed
CI / build-check-test (push) Has been cancelled
feat(sproutclaw): modularize webui, extensions, and agent config layout
Restructure local extensions into per-feature directories, split WebUI
into backend modules with slash commands and systemd support, and track
prompts/skills under .pi/agent for portable Gitea deployment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 16:57:08 +08:00

207 lines
7.1 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from "node:http";
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;
}
return false;
}