chore: add sproutclaw git workflow and track local extensions
Some checks failed
CI / build-check-test (push) Has been cancelled
Some checks failed
CI / build-check-test (push) Has been cancelled
Document main/upstream-sync/feature branch strategy, add sync/push scripts, track .pi/agent extensions and webui in git, and disable startup changelog via showChangelogOnStartup. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
224
.pi/agent/extensions/webui/db.ts
Normal file
224
.pi/agent/extensions/webui/db.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* WebUI 专有 SQLite 配置存储
|
||||
*
|
||||
* 数据库文件位于扩展目录 data/webui.db,与 pi-Agent 配置完全隔离。
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
const SCHEMA_VERSION = 1;
|
||||
|
||||
export interface WebuiDbInfo {
|
||||
dataDir: string;
|
||||
dbPath: string;
|
||||
}
|
||||
|
||||
export interface WebuiAvatarSettings {
|
||||
userAvatarUrl: string;
|
||||
agentAvatarUrl: string;
|
||||
}
|
||||
|
||||
let db: DatabaseSync | null = null;
|
||||
|
||||
function requireDb(): DatabaseSync {
|
||||
if (!db?.isOpen) {
|
||||
throw new Error("WebUI 数据库未初始化");
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
function getMeta(key: string): string | null {
|
||||
const row = requireDb()
|
||||
.prepare("SELECT value FROM webui_meta WHERE key = ?")
|
||||
.get(key) as { value: string } | undefined;
|
||||
return row?.value ?? null;
|
||||
}
|
||||
|
||||
function setMeta(key: string, value: string): void {
|
||||
requireDb()
|
||||
.prepare(
|
||||
`INSERT INTO webui_meta (key, value) VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
||||
)
|
||||
.run(key, value);
|
||||
}
|
||||
|
||||
function migrateLegacyJsonIfNeeded(extensionDir: string): void {
|
||||
if (getMeta("legacy_json_migrated") === "1") return;
|
||||
|
||||
const legacyFile = join(extensionDir, "webui-settings.json");
|
||||
if (!existsSync(legacyFile)) {
|
||||
setMeta("legacy_json_migrated", "1");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(legacyFile, "utf8")) as Partial<WebuiAvatarSettings>;
|
||||
if (typeof raw.userAvatarUrl === "string" && raw.userAvatarUrl && !getWebuiConfig("userAvatarUrl")) {
|
||||
setWebuiConfig("userAvatarUrl", raw.userAvatarUrl);
|
||||
}
|
||||
if (typeof raw.agentAvatarUrl === "string" && raw.agentAvatarUrl && !getWebuiConfig("agentAvatarUrl")) {
|
||||
setWebuiConfig("agentAvatarUrl", raw.agentAvatarUrl);
|
||||
}
|
||||
const migratedPath = `${legacyFile}.migrated`;
|
||||
if (!existsSync(migratedPath)) {
|
||||
renameSync(legacyFile, migratedPath);
|
||||
}
|
||||
console.log("[webui] 已从 webui-settings.json 迁移配置到 SQLite");
|
||||
} catch (err) {
|
||||
console.warn("[webui] 迁移 webui-settings.json 失败:", err);
|
||||
}
|
||||
|
||||
setMeta("legacy_json_migrated", "1");
|
||||
}
|
||||
|
||||
export function initWebuiDatabase(extensionDir: string): WebuiDbInfo {
|
||||
const dataDir = join(extensionDir, "data");
|
||||
const dbPath = join(dataDir, "webui.db");
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
|
||||
db = new DatabaseSync(dbPath);
|
||||
db.exec("PRAGMA journal_mode = WAL;");
|
||||
db.exec("PRAGMA foreign_keys = ON;");
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS webui_meta (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS webui_config (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
if (!getMeta("schema_version")) {
|
||||
setMeta("schema_version", String(SCHEMA_VERSION));
|
||||
}
|
||||
|
||||
migrateLegacyJsonIfNeeded(extensionDir);
|
||||
|
||||
return { dataDir, dbPath };
|
||||
}
|
||||
|
||||
export function getWebuiConfig(key: string): string | null {
|
||||
const row = requireDb()
|
||||
.prepare("SELECT value FROM webui_config WHERE key = ?")
|
||||
.get(key) as { value: string } | undefined;
|
||||
return row?.value ?? null;
|
||||
}
|
||||
|
||||
export function setWebuiConfig(key: string, value: string): void {
|
||||
const now = new Date().toISOString();
|
||||
requireDb()
|
||||
.prepare(
|
||||
`INSERT INTO webui_config (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
||||
)
|
||||
.run(key, value, now);
|
||||
}
|
||||
|
||||
export function setWebuiConfigMany(entries: Record<string, string>): void {
|
||||
const stmt = requireDb().prepare(
|
||||
`INSERT INTO webui_config (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
||||
);
|
||||
const now = new Date().toISOString();
|
||||
for (const [key, value] of Object.entries(entries)) {
|
||||
stmt.run(key, value, now);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllWebuiConfig(): Record<string, string> {
|
||||
const rows = requireDb()
|
||||
.prepare("SELECT key, value FROM webui_config ORDER BY key")
|
||||
.all() as Array<{ key: string; value: string }>;
|
||||
return Object.fromEntries(rows.map((row) => [row.key, row.value]));
|
||||
}
|
||||
|
||||
export function deleteWebuiConfig(key: string): boolean {
|
||||
const result = requireDb().prepare("DELETE FROM webui_config WHERE key = ?").run(key);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
export function readWebuiAvatarSettings(): WebuiAvatarSettings {
|
||||
return {
|
||||
userAvatarUrl: getWebuiConfig("userAvatarUrl") ?? "",
|
||||
agentAvatarUrl: getWebuiConfig("agentAvatarUrl") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
export function writeWebuiAvatarSettings(settings: WebuiAvatarSettings): void {
|
||||
setWebuiConfigMany({
|
||||
userAvatarUrl: settings.userAvatarUrl,
|
||||
agentAvatarUrl: settings.agentAvatarUrl,
|
||||
});
|
||||
}
|
||||
|
||||
export function closeWebuiDatabase(): void {
|
||||
if (db?.isOpen) {
|
||||
db.close();
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getWebuiDatabasePath(): string | null {
|
||||
return db?.location() ?? null;
|
||||
}
|
||||
|
||||
const PINNED_SESSIONS_CONFIG_KEY = "pinnedSessionPaths";
|
||||
|
||||
export function readPinnedSessionPaths(): string[] {
|
||||
const raw = getWebuiConfig(PINNED_SESSIONS_CONFIG_KEY);
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writePinnedSessionPaths(paths: string[]): void {
|
||||
const unique: string[] = [];
|
||||
for (const path of paths) {
|
||||
if (!unique.includes(path)) unique.push(path);
|
||||
}
|
||||
setWebuiConfig(PINNED_SESSIONS_CONFIG_KEY, JSON.stringify(unique));
|
||||
}
|
||||
|
||||
export function prunePinnedSessionPaths(validPaths: Iterable<string>): string[] {
|
||||
const valid = new Set(validPaths);
|
||||
const current = readPinnedSessionPaths();
|
||||
const pruned = current.filter((path) => valid.has(path));
|
||||
if (pruned.length !== current.length) {
|
||||
writePinnedSessionPaths(pruned);
|
||||
}
|
||||
return pruned;
|
||||
}
|
||||
|
||||
export function pinSessionPath(path: string): string[] {
|
||||
const current = readPinnedSessionPaths();
|
||||
if (current.includes(path)) return current;
|
||||
writePinnedSessionPaths([path, ...current]);
|
||||
return readPinnedSessionPaths();
|
||||
}
|
||||
|
||||
export function unpinSessionPath(path: string): string[] {
|
||||
const next = readPinnedSessionPaths().filter((entry) => entry !== path);
|
||||
writePinnedSessionPaths(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function removePinnedSessionPath(path: string): void {
|
||||
if (readPinnedSessionPaths().includes(path)) {
|
||||
unpinSessionPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
export function setSessionPinned(path: string, pinned: boolean): string[] {
|
||||
return pinned ? pinSessionPath(path) : unpinSessionPath(path);
|
||||
}
|
||||
Reference in New Issue
Block a user