feat(sproutclaw): modularize webui, extensions, and agent config layout
Some checks failed
CI / build-check-test (push) Has been cancelled
Some checks failed
CI / build-check-test (push) Has been cancelled
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>
This commit is contained in:
206
.pi/agent/extensions/webui/backend/routes/settings.ts
Normal file
206
.pi/agent/extensions/webui/backend/routes/settings.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user