182 lines
5.3 KiB
JavaScript
182 lines
5.3 KiB
JavaScript
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const DATA_DIR = path.resolve(__dirname, '..', 'data');
|
|
const DB_PATH = path.join(DATA_DIR, 'studyos.db');
|
|
|
|
if (!fs.existsSync(DATA_DIR)) {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
let _sqlDb = null;
|
|
|
|
function saveToDisk() {
|
|
if (!_sqlDb) return;
|
|
const data = _sqlDb.export();
|
|
fs.writeFileSync(DB_PATH, Buffer.from(data));
|
|
}
|
|
|
|
function flatParams(params) {
|
|
if (params.length === 1 && Array.isArray(params[0])) return params[0];
|
|
return params;
|
|
}
|
|
|
|
function createWrapper(sqlDb) {
|
|
return {
|
|
prepare(sql) {
|
|
return {
|
|
run(...params) {
|
|
const p = flatParams(params);
|
|
sqlDb.run(sql, p);
|
|
const rowidResult = sqlDb.exec('SELECT last_insert_rowid()');
|
|
const lastInsertRowid = rowidResult.length > 0 ? rowidResult[0].values[0][0] : 0;
|
|
const changes = sqlDb.getRowsModified();
|
|
saveToDisk();
|
|
return { changes, lastInsertRowid };
|
|
},
|
|
get(...params) {
|
|
const p = flatParams(params);
|
|
let stmt = null;
|
|
try {
|
|
stmt = sqlDb.prepare(sql);
|
|
stmt.bind(p);
|
|
let result = null;
|
|
if (stmt.step()) result = stmt.getAsObject();
|
|
return result;
|
|
} finally {
|
|
if (stmt) stmt.free();
|
|
}
|
|
},
|
|
all(...params) {
|
|
const p = flatParams(params);
|
|
let stmt = null;
|
|
try {
|
|
stmt = sqlDb.prepare(sql);
|
|
stmt.bind(p);
|
|
const results = [];
|
|
while (stmt.step()) results.push(stmt.getAsObject());
|
|
return results;
|
|
} finally {
|
|
if (stmt) stmt.free();
|
|
}
|
|
},
|
|
};
|
|
},
|
|
exec(sql) {
|
|
sqlDb.exec(sql);
|
|
saveToDisk();
|
|
},
|
|
pragma(sql) {
|
|
sqlDb.exec('PRAGMA ' + sql + ';');
|
|
},
|
|
};
|
|
}
|
|
|
|
const db = createWrapper(null);
|
|
|
|
async function initDB() {
|
|
const SQL = await import('sql.js');
|
|
const initSqlJs = SQL.default;
|
|
const sqlModule = await initSqlJs();
|
|
|
|
let sqlDb;
|
|
if (fs.existsSync(DB_PATH)) {
|
|
const buffer = fs.readFileSync(DB_PATH);
|
|
sqlDb = new sqlModule.Database(buffer);
|
|
} else {
|
|
sqlDb = new sqlModule.Database();
|
|
}
|
|
|
|
_sqlDb = sqlDb;
|
|
|
|
const real = createWrapper(sqlDb);
|
|
db.prepare = real.prepare;
|
|
db.exec = real.exec;
|
|
db.pragma = real.pragma;
|
|
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS models (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
api_base TEXT NOT NULL,
|
|
api_key TEXT NOT NULL DEFAULT '',
|
|
provider TEXT NOT NULL CHECK(provider IN ('openai', 'anthropic')),
|
|
is_default_main INTEGER NOT NULL DEFAULT 0,
|
|
is_default_fork INTEGER NOT NULL DEFAULT 0,
|
|
is_default_exam INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
type TEXT NOT NULL CHECK(type IN ('main', 'fork')),
|
|
parent_id INTEGER REFERENCES conversations(id) ON DELETE SET NULL,
|
|
model_id INTEGER REFERENCES models(id) ON DELETE SET NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'context_merge')),
|
|
content TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pdfs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
filename TEXT NOT NULL,
|
|
original_name TEXT NOT NULL,
|
|
content_markdown TEXT NOT NULL DEFAULT '',
|
|
pages INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
reorder_index INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS progress (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
topic TEXT NOT NULL UNIQUE,
|
|
exercises_done INTEGER NOT NULL DEFAULT 0,
|
|
exercises_correct INTEGER NOT NULL DEFAULT 0,
|
|
last_session TEXT,
|
|
notes TEXT NOT NULL DEFAULT '[]'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS notes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
content_markdown TEXT NOT NULL DEFAULT '',
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL DEFAULT ''
|
|
);
|
|
`);
|
|
|
|
const modelCount = db.prepare('SELECT COUNT(*) as count FROM models').get();
|
|
if (!modelCount || modelCount.count === 0) {
|
|
db.prepare(`
|
|
INSERT INTO models (name, api_base, api_key, provider, is_default_main)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run('claude-sonnet-4', 'https://api.anthropic.com', '', 'anthropic', 1);
|
|
}
|
|
|
|
const vlmConfig = db.prepare("SELECT value FROM config WHERE key = ?").get('vlm_endpoint');
|
|
if (!vlmConfig) {
|
|
db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run('vlm_endpoint', 'http://localhost:8080/vlm');
|
|
}
|
|
|
|
return db;
|
|
}
|
|
|
|
module.exports = db;
|
|
module.exports.initDB = initDB;
|