From 935656dc4f252ab9643a091a4fb7e9cc95ddb79d Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Thu, 7 May 2026 20:10:06 +0800 Subject: [PATCH] feat(api): /api/bot/:id/avatar GET/PUT/DELETE Co-Authored-By: Claude Sonnet 4.6 --- src/index.ts | 1 + src/web/api/bot.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++- src/web/server.ts | 13 +++++++-- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index cff9f6d..e107dcf 100755 --- a/src/index.ts +++ b/src/index.ts @@ -74,6 +74,7 @@ async function main() { qqProvider, bilibiliProvider, database: db, + avatarStore, config, configPath: CONFIG_PATH, logger, diff --git a/src/web/api/bot.ts b/src/web/api/bot.ts index 50a135f..51a417b 100755 --- a/src/web/api/bot.ts +++ b/src/web/api/bot.ts @@ -3,12 +3,16 @@ import type { BotManager } from "../../bot/manager.js"; import type { BotConfig } from "../../data/config.js"; import { saveConfig } from "../../data/config.js"; import type { Logger } from "../../logger.js"; +import type { BotDatabase } from "../../data/database.js"; +import type { AvatarStore } from "../../data/avatars.js"; export function createBotRouter( botManager: BotManager, config: BotConfig, configPath: string, - logger: Logger + logger: Logger, + botDb: BotDatabase, + avatarStore: AvatarStore, ): Router { const router = Router(); @@ -36,6 +40,70 @@ export function createBotRouter( res.json(saved); }); + router.get("/:id/avatar", (req, res) => { + const path = botDb.getCustomAvatarPath(req.params.id); + if (!path) { + res.status(404).end(); + return; + } + const buf = avatarStore.read(path); + if (!buf) { + res.status(404).end(); + return; + } + const ext = path.split(".").pop() ?? ""; + const mime = ext === "png" + ? "image/png" + : ext === "webp" + ? "image/webp" + : "image/jpeg"; + res.set("Content-Type", mime); + res.set("Cache-Control", "no-cache"); + res.send(buf); + }); + + router.put("/:id/avatar", (req, res) => { + const exists = + botManager.getBot(req.params.id) || + botDb.getBotInstances().some((b) => b.id === req.params.id); + if (!exists) { + res.status(404).json({ error: "Bot not found" }); + return; + } + const { dataUrl } = req.body as { dataUrl?: string }; + if (typeof dataUrl !== "string") { + res.status(400).json({ error: "dataUrl required" }); + return; + } + const m = /^data:(image\/(?:png|jpeg|webp));base64,(.+)$/.exec(dataUrl); + if (!m) { + res.status(400).json({ error: "dataUrl must be image/png|jpeg|webp base64" }); + return; + } + const mime = m[1] as string; + const buf = Buffer.from(m[2] ?? "", "base64"); + if (buf.length === 0) { + res.status(400).json({ error: "empty image" }); + return; + } + if (buf.length > 200 * 1024) { + res.status(413).json({ error: "avatar exceeds 200KB limit" }); + return; + } + const rel = avatarStore.write(req.params.id, mime, buf); + botDb.setCustomAvatarPath(req.params.id, rel); + botManager.getBot(req.params.id)?.getProfileManager().setCustomAvatar(buf); + res.json({ path: rel }); + }); + + router.delete("/:id/avatar", (req, res) => { + const path = botDb.getCustomAvatarPath(req.params.id); + if (path) avatarStore.remove(path); + botDb.setCustomAvatarPath(req.params.id, null); + botManager.getBot(req.params.id)?.getProfileManager().setCustomAvatar(null); + res.status(204).end(); + }); + router.post("/", async (req, res) => { try { const { diff --git a/src/web/server.ts b/src/web/server.ts index 90b7dec..b496c02 100755 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -8,6 +8,7 @@ import type { BotDatabase } from "../data/database.js"; import type { BotConfig } from "../data/config.js"; import type { Logger } from "../logger.js"; import type { CookieStore } from "../music/auth.js"; +import type { AvatarStore } from "../data/avatars.js"; import { createBotRouter } from "./api/bot.js"; import { createMusicRouter } from "./api/music.js"; import { createPlayerRouter } from "./api/player.js"; @@ -25,6 +26,7 @@ export interface WebServerOptions { configPath: string; logger: Logger; cookieStore?: CookieStore; + avatarStore: AvatarStore; staticDir?: string; } @@ -43,7 +45,7 @@ export function createWebServer(options: WebServerOptions): WebServer { app.set("trust proxy", true); } - app.use(express.json()); + app.use(express.json({ limit: "400kb" })); app.get("/api/config/public-url", (_req, res) => { const raw = (options.config.publicUrl ?? "").trim(); @@ -52,7 +54,14 @@ export function createWebServer(options: WebServerOptions): WebServer { app.use( "/api/bot", - createBotRouter(options.botManager, options.config, options.configPath, logger) + createBotRouter( + options.botManager, + options.config, + options.configPath, + logger, + options.database, + options.avatarStore, + ) ); app.use( "/api/music",