feat(api): /api/bot/:id/avatar GET/PUT/DELETE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Sonnet 4.6 committed 2026-05-07 20:10:06 +08:00
1 parent 2dd6a9e20d
commit 935656dc4f
3 files changed
+81 -3

No files matched your search

+1
View File
@@ -74,6 +74,7 @@ async function main() {
qqProvider,
bilibiliProvider,
database: db,
avatarStore,
config,
configPath: CONFIG_PATH,
logger,
+69 -1
View File
@@ -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 {
+11 -2
View File
@@ -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",