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:
76
.pi/agent/extensions/webui/backend/routes/chat.ts
Normal file
76
.pi/agent/extensions/webui/backend/routes/chat.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { dispatchSlashCommand } from "../slash/dispatch.ts";
|
||||
import { normalizeChatImages } from "../services/chat-images.ts";
|
||||
import type { WebUiContext } from "../types/context.ts";
|
||||
import { json, readBody } from "../http/request.ts";
|
||||
|
||||
export function handleChatRoute(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
ctx: WebUiContext,
|
||||
pathname: string,
|
||||
): boolean {
|
||||
const { sendCmd, submitPrompt } = ctx.rpc;
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/chat") {
|
||||
void readBody(req)
|
||||
.then(async ({ message, images }) => {
|
||||
const msg = typeof message === "string" ? message : "";
|
||||
const imgs = normalizeChatImages(images);
|
||||
if (!msg.trim() && !imgs?.length) throw new Error("消息不能为空");
|
||||
if (imgs?.length) console.log(`[webui] chat: ${imgs.length} image(s), message=${msg.length} chars`);
|
||||
|
||||
if (!imgs?.length && msg.trim().startsWith("/")) {
|
||||
const slash = await dispatchSlashCommand(msg, sendCmd);
|
||||
if (slash?.handled) {
|
||||
return json(res, { ok: true, slash: true, ...slash });
|
||||
}
|
||||
}
|
||||
|
||||
submitPrompt({ message: msg, images: imgs });
|
||||
return json(res, { ok: true, accepted: true }, 202);
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/bash") {
|
||||
void readBody(req)
|
||||
.then(async ({ command }) => {
|
||||
const cmd = typeof command === "string" ? command.trim() : "";
|
||||
if (!cmd) throw new Error("命令不能为空");
|
||||
const result = await sendCmd({ type: "bash", command: cmd });
|
||||
if (!result.success) throw new Error(result.error || "命令执行失败");
|
||||
json(res, result.data);
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/events") {
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
});
|
||||
ctx.rpc.connectSseClient(res);
|
||||
req.on("close", () => ctx.rpc.removeSseClient(res));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/messages") {
|
||||
void sendCmd({ type: "get_messages" })
|
||||
.then((r: any) => (r.success ? json(res, r.data) : json(res, { error: r.error }, 500)))
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/abort") {
|
||||
void sendCmd({ type: "abort" })
|
||||
.then(() => json(res, { ok: true }))
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
19
.pi/agent/extensions/webui/backend/routes/commands.ts
Normal file
19
.pi/agent/extensions/webui/backend/routes/commands.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { listSlashCommands } from "../services/slash-commands.ts";
|
||||
import type { WebUiContext } from "../types/context.ts";
|
||||
import { json } from "../http/request.ts";
|
||||
|
||||
export function handleCommandsRoute(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
ctx: WebUiContext,
|
||||
pathname: string,
|
||||
): boolean {
|
||||
if (req.method === "GET" && pathname === "/api/commands") {
|
||||
void listSlashCommands(ctx.rpc.sendCmd)
|
||||
.then((commands) => json(res, { commands }))
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
43
.pi/agent/extensions/webui/backend/routes/models.ts
Normal file
43
.pi/agent/extensions/webui/backend/routes/models.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { WebUiContext } from "../types/context.ts";
|
||||
import { json, readBody } from "../http/request.ts";
|
||||
|
||||
export function handleModelsRoute(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
ctx: WebUiContext,
|
||||
pathname: string,
|
||||
): boolean {
|
||||
const { sendCmd } = ctx.rpc;
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/models") {
|
||||
void sendCmd({ type: "get_available_models" })
|
||||
.then((r: any) => (r.success ? json(res, r.data) : json(res, { error: r.error }, 500)))
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/model") {
|
||||
void readBody(req)
|
||||
.then(({ provider, modelId }) =>
|
||||
sendCmd({ type: "set_model", provider, modelId }).then((r: any) =>
|
||||
r.success ? json(res, { model: r.data }) : json(res, { error: r.error }, 500),
|
||||
),
|
||||
)
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/thinking") {
|
||||
void readBody(req)
|
||||
.then(({ level }) =>
|
||||
sendCmd({ type: "set_thinking_level", level }).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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
124
.pi/agent/extensions/webui/backend/routes/sessions.ts
Normal file
124
.pi/agent/extensions/webui/backend/routes/sessions.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import {
|
||||
appendSessionName,
|
||||
buildSessionListResponse,
|
||||
deleteSessionFile,
|
||||
pinSession,
|
||||
readSessionMessages,
|
||||
readSessionSummary,
|
||||
resolveSessionFile,
|
||||
} from "../services/sessions.ts";
|
||||
import type { WebUiContext } from "../types/context.ts";
|
||||
import { json, readBody } from "../http/request.ts";
|
||||
|
||||
export function handleSessionsRoute(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
ctx: WebUiContext,
|
||||
pathname: string,
|
||||
): boolean {
|
||||
const { paths } = ctx.config;
|
||||
const { sendCmd } = ctx.rpc;
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/new-session") {
|
||||
void sendCmd({ type: "new_session" })
|
||||
.then(async (r: any) => {
|
||||
if (!r.success) return json(res, { error: r.error }, 500);
|
||||
if (r.data?.cancelled) return json(res, r.data);
|
||||
const state = await sendCmd({ type: "get_state" });
|
||||
const sessionFile = state.success ? state.data?.sessionFile : undefined;
|
||||
return json(res, { ...r.data, sessionFile });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/sessions") {
|
||||
json(res, buildSessionListResponse(paths));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/sessions/history") {
|
||||
void readBody(req)
|
||||
.then(({ path: sp }) => {
|
||||
const messages = readSessionMessages(sp as string);
|
||||
const summary = readSessionSummary(paths, sp as string);
|
||||
json(res, { messages, session: summary });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/sessions/delete") {
|
||||
void readBody(req)
|
||||
.then(({ path: sp }) => {
|
||||
deleteSessionFile(paths, sp as string);
|
||||
json(res, { ok: true });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/sessions/pin") {
|
||||
void readBody(req)
|
||||
.then(({ path: sp, pinned }) => {
|
||||
const result = pinSession(paths, sp as string, Boolean(pinned));
|
||||
json(res, { ok: true, ...result });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/sessions/load") {
|
||||
void readBody(req)
|
||||
.then(async ({ path: sp }) => {
|
||||
const sw = await sendCmd({ type: "switch_session", sessionPath: sp, cwdOverride: paths.repoRoot });
|
||||
if (!sw.success) throw new Error(sw.error);
|
||||
const mr = await sendCmd({ type: "get_messages" });
|
||||
if (!mr.success) throw new Error(mr.error);
|
||||
const summary = readSessionSummary(paths, sp as string);
|
||||
json(res, { messages: mr.data.messages, session: summary });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/sessions/activate") {
|
||||
void readBody(req)
|
||||
.then(async ({ path: sp }) => {
|
||||
const sw = await sendCmd({ type: "switch_session", sessionPath: sp, cwdOverride: paths.repoRoot });
|
||||
if (!sw.success) throw new Error(sw.error);
|
||||
const state = await sendCmd({ type: "get_state" });
|
||||
if (!state.success) throw new Error(state.error);
|
||||
json(res, { ok: true, state: state.data });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/sessions/name") {
|
||||
void readBody(req)
|
||||
.then(async ({ path: sp, name }) => {
|
||||
if (typeof sp !== "string" || !sp.trim()) throw new Error("无效会话路径");
|
||||
const savedName = appendSessionName(paths, sp, typeof name === "string" ? name : "");
|
||||
const sessionPath = resolveSessionFile(paths, sp);
|
||||
const state = await sendCmd({ type: "get_state" });
|
||||
if (state.success && state.data?.sessionFile === sessionPath) {
|
||||
const rename = await sendCmd({ type: "set_session_name", name: savedName });
|
||||
if (!rename.success) throw new Error(rename.error || "同步当前会话名称失败");
|
||||
}
|
||||
json(res, { ok: true, name: savedName });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/session-state") {
|
||||
void sendCmd({ type: "get_state" })
|
||||
.then((r: any) => (r.success ? json(res, r.data) : json(res, { error: r.error }, 500)))
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
70
.pi/agent/extensions/webui/backend/routes/webui-config.ts
Normal file
70
.pi/agent/extensions/webui/backend/routes/webui-config.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import {
|
||||
deleteWebuiConfig,
|
||||
getAllWebuiConfig,
|
||||
getWebuiConfig,
|
||||
readWebuiAvatarSettings,
|
||||
setWebuiConfig,
|
||||
setWebuiConfigMany,
|
||||
} from "../db/index.ts";
|
||||
import { normalizeConfigKey, normalizeConfigValue } from "../services/avatars.ts";
|
||||
import type { WebUiContext } from "../types/context.ts";
|
||||
import { json, readBody } from "../http/request.ts";
|
||||
|
||||
export function handleWebuiConfigRoute(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
ctx: WebUiContext,
|
||||
pathname: string,
|
||||
url: URL,
|
||||
): boolean {
|
||||
if (req.method === "GET" && pathname === "/api/avatars") {
|
||||
json(res, readWebuiAvatarSettings());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/webui/config") {
|
||||
const key = url.searchParams.get("key");
|
||||
if (key) {
|
||||
const normalized = normalizeConfigKey(key);
|
||||
json(res, { key: normalized, value: getWebuiConfig(normalized) });
|
||||
return true;
|
||||
}
|
||||
json(res, { config: getAllWebuiConfig(), dbPath: ctx.db.dbPath });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/webui/config") {
|
||||
void readBody(req)
|
||||
.then(({ key, value, entries }) => {
|
||||
if (entries && typeof entries === "object" && !Array.isArray(entries)) {
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const [rawKey, rawValue] of Object.entries(entries as Record<string, unknown>)) {
|
||||
normalized[normalizeConfigKey(rawKey)] = normalizeConfigValue(rawValue);
|
||||
}
|
||||
setWebuiConfigMany(normalized);
|
||||
return json(res, { ok: true, config: getAllWebuiConfig() });
|
||||
}
|
||||
if (typeof key !== "string") throw new Error("缺少配置键 key");
|
||||
const normalizedKey = normalizeConfigKey(key);
|
||||
const normalizedValue = normalizeConfigValue(value);
|
||||
setWebuiConfig(normalizedKey, normalizedValue);
|
||||
return json(res, { ok: true, key: normalizedKey, value: normalizedValue });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/webui/config/delete") {
|
||||
void readBody(req)
|
||||
.then(({ key }) => {
|
||||
const normalized = normalizeConfigKey(key);
|
||||
const deleted = deleteWebuiConfig(normalized);
|
||||
json(res, { ok: true, deleted, key: normalized });
|
||||
})
|
||||
.catch((err) => json(res, { error: err.message }, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user