feat: 全局设置系统 — settings 表、CRUD 路由、主题偏好持久化
This commit is contained in:
@@ -20,4 +20,5 @@ export {
|
||||
listModels,
|
||||
updateModel,
|
||||
} from "./models";
|
||||
export { conversations, messages, projects, schemaMigrations } from "./schema";
|
||||
export { conversations, messages, projects, schemaMigrations, settings } from "./schema";
|
||||
export { getSettings, updateSettings } from "./settings";
|
||||
|
||||
@@ -86,3 +86,8 @@ export const schemaMigrations = sqliteTable("schema_migrations", {
|
||||
checksum: text("checksum").notNull(),
|
||||
id: text("id").primaryKey(),
|
||||
});
|
||||
|
||||
export const settings = sqliteTable("settings", {
|
||||
...baseColumns,
|
||||
data: text("data").notNull().default("{}"),
|
||||
});
|
||||
|
||||
72
src/server/db/settings.ts
Normal file
72
src/server/db/settings.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
import type { SettingsData } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { notDeleted, timestamp, wrap } from "./connection";
|
||||
import { settings } from "./schema";
|
||||
|
||||
const SETTINGS_ID = "default";
|
||||
|
||||
export function getSettings(raw: Database): SettingsData {
|
||||
const db = wrap(raw);
|
||||
const row = db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(and(eq(settings.id, SETTINGS_ID), notDeleted(settings)))
|
||||
.get();
|
||||
|
||||
if (!row) {
|
||||
return { theme: "system" };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(row.data) as Partial<SettingsData>;
|
||||
return {
|
||||
theme: parsed.theme === "dark" || parsed.theme === "light" || parsed.theme === "system" ? parsed.theme : "system",
|
||||
};
|
||||
} catch {
|
||||
return { theme: "system" };
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSettings(raw: Database, data: Partial<SettingsData>, _logger: Logger): SettingsData {
|
||||
const db = wrap(raw);
|
||||
const existing = db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(and(eq(settings.id, SETTINGS_ID), notDeleted(settings)))
|
||||
.get();
|
||||
|
||||
let currentData: SettingsData = { theme: "system" };
|
||||
if (existing) {
|
||||
try {
|
||||
currentData = JSON.parse(existing.data) as SettingsData;
|
||||
} catch {
|
||||
// 解析失败时使用默认值
|
||||
}
|
||||
}
|
||||
|
||||
const merged: SettingsData = { ...currentData, ...data };
|
||||
|
||||
if (existing) {
|
||||
db.update(settings)
|
||||
.set({ data: JSON.stringify(merged), updatedAt: timestamp() })
|
||||
.where(eq(settings.id, SETTINGS_ID))
|
||||
.run();
|
||||
} else {
|
||||
const now = timestamp();
|
||||
db.insert(settings)
|
||||
.values({
|
||||
createdAt: now,
|
||||
data: JSON.stringify(merged),
|
||||
id: SETTINGS_ID,
|
||||
updatedAt: now,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
38
src/server/routes/settings.ts
Normal file
38
src/server/routes/settings.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode, SettingsData } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { getSettings, updateSettings } from "../db/settings";
|
||||
import { createApiError, jsonResponse } from "../helpers";
|
||||
|
||||
export function handleGetSettings(_req: Request, db: Database, mode: RuntimeMode, _logger: Logger): Response {
|
||||
const data = getSettings(db);
|
||||
return jsonResponse(data, { mode });
|
||||
}
|
||||
|
||||
export async function handleUpdateSettings(
|
||||
req: Request,
|
||||
db: Database,
|
||||
mode: RuntimeMode,
|
||||
logger: Logger,
|
||||
): Promise<Response> {
|
||||
let body: Partial<SettingsData>;
|
||||
try {
|
||||
body = (await req.json()) as Partial<SettingsData>;
|
||||
} catch {
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (body.theme !== undefined && typeof body.theme !== "string") {
|
||||
return jsonResponse(createApiError("theme must be a string", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (body.theme !== undefined && body.theme !== "dark" && body.theme !== "light" && body.theme !== "system") {
|
||||
return jsonResponse(createApiError("theme 仅支持 dark、light、system", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
const result = updateSettings(db, body, logger);
|
||||
logger.info({ data: result }, "设置已更新");
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
@@ -330,6 +330,24 @@ export function startServer(options: StartServerOptions) {
|
||||
logger,
|
||||
),
|
||||
},
|
||||
"/api/settings": {
|
||||
GET: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleGetSettings } = await import("./routes/settings");
|
||||
return handleGetSettings(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
),
|
||||
PUT: withErrorHandler(
|
||||
async (req) => {
|
||||
const { handleUpdateSettings } = await import("./routes/settings");
|
||||
return handleUpdateSettings(req, db, mode, logger);
|
||||
},
|
||||
mode,
|
||||
logger,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user