feat: add SQLite database module with play history and bot instances

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 00:21:07 +08:00
1 parent 6324953394
commit 8727605036
2 files changed
+225

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createDatabase, type BotDatabase, type BotInstance } from "./database.js";
describe("database", () => {
let botDb: BotDatabase;
beforeEach(() => {
botDb = createDatabase(":memory:");
});
afterEach(() => {
botDb.close();
});
it("creates tables on init", () => {
const tables = botDb.db
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
.all() as Array<{ name: string }>;
const names = tables.map((t) => t.name);
expect(names).toContain("play_history");
expect(names).toContain("bot_instances");
});
it("records and retrieves play history", () => {
botDb.addPlayHistory({
botId: "bot1",
songId: "song1",
songName: "Test Song",
artist: "Test Artist",
album: "Test Album",
platform: "netease",
coverUrl: "https://example.com/cover.jpg",
playedAt: new Date().toISOString(),
});
botDb.addPlayHistory({
botId: "bot1",
songId: "song2",
songName: "Another Song",
artist: "Another Artist",
album: "Another Album",
platform: "qq",
coverUrl: "https://example.com/cover2.jpg",
playedAt: new Date().toISOString(),
});
const history = botDb.getPlayHistory("bot1", 10);
expect(history).toHaveLength(2);
expect(history[0].songName).toBe("Another Song");
expect(history[1].songName).toBe("Test Song");
});
it("saves and loads bot instances", () => {
const instance: BotInstance = {
id: "bot1",
name: "Music Bot",
serverAddress: "localhost",
serverPort: 9987,
nickname: "MusicBot",
defaultChannel: "Music",
channelPassword: "",
autoStart: true,
};
botDb.saveBotInstance(instance);
const instances = botDb.getBotInstances();
expect(instances).toHaveLength(1);
expect(instances[0]).toEqual(instance);
expect(instances[0].autoStart).toBe(true);
// Test upsert
botDb.saveBotInstance({ ...instance, nickname: "UpdatedBot", autoStart: false });
const updated = botDb.getBotInstances();
expect(updated).toHaveLength(1);
expect(updated[0].nickname).toBe("UpdatedBot");
expect(updated[0].autoStart).toBe(false);
});
it("deletes bot instance", () => {
botDb.saveBotInstance({
id: "bot1",
name: "Music Bot",
serverAddress: "localhost",
serverPort: 9987,
nickname: "MusicBot",
defaultChannel: "Music",
channelPassword: "",
autoStart: false,
});
expect(botDb.deleteBotInstance("bot1")).toBe(true);
expect(botDb.getBotInstances()).toHaveLength(0);
expect(botDb.deleteBotInstance("nonexistent")).toBe(false);
});
});
+128
View File
@@ -0,0 +1,128 @@
import Database from "better-sqlite3";
export interface PlayHistoryRecord {
id?: number;
botId: string;
songId: string;
songName: string;
artist: string;
album: string;
platform: string;
coverUrl: string;
playedAt: string;
}
export interface BotInstance {
id: string;
name: string;
serverAddress: string;
serverPort: number;
nickname: string;
defaultChannel: string;
channelPassword: string;
autoStart: boolean;
}
export interface BotDatabase {
db: Database.Database;
addPlayHistory(record: Omit<PlayHistoryRecord, "id">): void;
getPlayHistory(botId: string, limit: number): PlayHistoryRecord[];
saveBotInstance(instance: BotInstance): void;
getBotInstances(): BotInstance[];
deleteBotInstance(id: string): boolean;
close(): void;
}
function initTables(db: Database.Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS play_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
botId TEXT NOT NULL,
songId TEXT NOT NULL,
songName TEXT NOT NULL,
artist TEXT NOT NULL,
album TEXT NOT NULL,
platform TEXT NOT NULL,
coverUrl TEXT NOT NULL,
playedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS bot_instances (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
serverAddress TEXT NOT NULL,
serverPort INTEGER NOT NULL,
nickname TEXT NOT NULL,
defaultChannel TEXT NOT NULL,
channelPassword TEXT NOT NULL,
autoStart INTEGER NOT NULL DEFAULT 0
);
`);
}
export function createDatabase(dbPath: string): BotDatabase {
const db = new Database(dbPath);
db.pragma("journal_mode = WAL");
initTables(db);
const insertHistory = db.prepare(`
INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl, playedAt)
VALUES (@botId, @songId, @songName, @artist, @album, @platform, @coverUrl, @playedAt)
`);
const selectHistory = db.prepare(`
SELECT * FROM play_history WHERE botId = ? ORDER BY id DESC LIMIT ?
`);
const upsertInstance = db.prepare(`
INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart)
VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
serverAddress = excluded.serverAddress,
serverPort = excluded.serverPort,
nickname = excluded.nickname,
defaultChannel = excluded.defaultChannel,
channelPassword = excluded.channelPassword,
autoStart = excluded.autoStart
`);
const selectInstances = db.prepare(`SELECT * FROM bot_instances`);
const deleteInstance = db.prepare(`DELETE FROM bot_instances WHERE id = ?`);
return {
db,
addPlayHistory(record) {
insertHistory.run(record);
},
getPlayHistory(botId, limit) {
return selectHistory.all(botId, limit) as PlayHistoryRecord[];
},
saveBotInstance(instance) {
upsertInstance.run({
...instance,
autoStart: instance.autoStart ? 1 : 0,
});
},
getBotInstances() {
const rows = selectInstances.all() as Array<
Omit<BotInstance, "autoStart"> & { autoStart: number }
>;
return rows.map((r) => ({ ...r, autoStart: r.autoStart === 1 }));
},
deleteBotInstance(id) {
const result = deleteInstance.run(id);
return result.changes > 0;
},
close() {
db.close();
},
};
}