315 lines
9.8 KiB
TypeScript
315 lines
9.8 KiB
TypeScript
import * as path from 'path';
|
|
import { app } from 'electron';
|
|
import Database from 'better-sqlite3';
|
|
import log from 'electron-log';
|
|
import { APP_DATA_DIR_NAME } from './services/constants';
|
|
import { APP_NAME_IDENTIFIER } from '@shared/constants';
|
|
|
|
const qimingHome = path.join(app.getPath('home'), APP_DATA_DIR_NAME);
|
|
const dbPath = path.join(qimingHome, `${APP_NAME_IDENTIFIER}.db`);
|
|
|
|
let db: Database.Database | null = null;
|
|
|
|
const DIGITAL_EMPLOYEE_SCHEMA_SQL = `
|
|
CREATE TABLE IF NOT EXISTS digital_plans (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
title TEXT NOT NULL,
|
|
objective TEXT,
|
|
status TEXT NOT NULL,
|
|
source TEXT NOT NULL DEFAULT 'local',
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_tasks (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT,
|
|
title TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
assigned_skill_id TEXT,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_plan_steps (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
seq INTEGER NOT NULL,
|
|
depends_on TEXT NOT NULL DEFAULT '[]',
|
|
preferred_skill_ids TEXT NOT NULL DEFAULT '[]',
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_schedules (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
cron_expr TEXT NOT NULL,
|
|
timezone TEXT NOT NULL DEFAULT 'system',
|
|
status TEXT NOT NULL DEFAULT 'enabled',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
prompt TEXT,
|
|
command TEXT,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
next_run_at TEXT,
|
|
last_run_at TEXT,
|
|
catch_up_on_startup INTEGER NOT NULL DEFAULT 1,
|
|
max_catch_up_runs INTEGER NOT NULL DEFAULT 5,
|
|
max_run_history INTEGER NOT NULL DEFAULT 100,
|
|
failure_count INTEGER NOT NULL DEFAULT 0,
|
|
last_error TEXT,
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
deleted_at TEXT,
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_schedule_runs (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
schedule_id TEXT NOT NULL,
|
|
plan_id TEXT,
|
|
run_id TEXT,
|
|
due_at TEXT NOT NULL,
|
|
started_at TEXT,
|
|
finished_at TEXT,
|
|
status TEXT NOT NULL,
|
|
attempt_no INTEGER NOT NULL DEFAULT 1,
|
|
trigger_kind TEXT NOT NULL DEFAULT 'scheduled',
|
|
error TEXT,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(schedule_id) REFERENCES digital_schedules(id),
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id),
|
|
FOREIGN KEY(run_id) REFERENCES digital_runs(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_runs (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT,
|
|
task_id TEXT,
|
|
status TEXT NOT NULL,
|
|
started_at TEXT,
|
|
finished_at TEXT,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id),
|
|
FOREIGN KEY(task_id) REFERENCES digital_tasks(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_events (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT,
|
|
task_id TEXT,
|
|
run_id TEXT,
|
|
kind TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
occurred_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id),
|
|
FOREIGN KEY(task_id) REFERENCES digital_tasks(id),
|
|
FOREIGN KEY(run_id) REFERENCES digital_runs(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_artifacts (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT,
|
|
task_id TEXT,
|
|
run_id TEXT,
|
|
kind TEXT NOT NULL,
|
|
name TEXT,
|
|
uri TEXT,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_approvals (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT,
|
|
task_id TEXT,
|
|
run_id TEXT,
|
|
title TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_memories (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
key TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
category TEXT NOT NULL DEFAULT 'fact',
|
|
source TEXT NOT NULL DEFAULT 'qimingclaw',
|
|
source_path TEXT,
|
|
session_id TEXT,
|
|
score REAL,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
deleted_at TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_sync_outbox (
|
|
id TEXT PRIMARY KEY,
|
|
entity_type TEXT NOT NULL,
|
|
entity_id TEXT NOT NULL,
|
|
operation TEXT NOT NULL,
|
|
payload TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
last_attempt_at TEXT,
|
|
error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_sync_attempts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
outbox_id TEXT NOT NULL,
|
|
entity_type TEXT NOT NULL,
|
|
entity_id TEXT NOT NULL,
|
|
operation TEXT NOT NULL,
|
|
attempt_no INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
error TEXT,
|
|
endpoint TEXT,
|
|
started_at TEXT NOT NULL,
|
|
finished_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_digital_plans_sync ON digital_plans(sync_status);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_plan_steps_plan ON digital_plan_steps(plan_id, seq);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_plan_steps_sync ON digital_plan_steps(sync_status);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedules_due ON digital_schedules(enabled, status, next_run_at);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedules_plan ON digital_schedules(plan_id);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedules_sync ON digital_schedules(sync_status);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedule_runs_schedule ON digital_schedule_runs(schedule_id, due_at);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_digital_schedule_runs_attempt ON digital_schedule_runs(schedule_id, due_at, attempt_no);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedule_runs_sync ON digital_schedule_runs(sync_status);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_tasks_plan ON digital_tasks(plan_id);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_runs_task ON digital_runs(task_id);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_events_plan ON digital_events(plan_id, occurred_at);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_memories_key ON digital_memories(key, status, updated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_memories_category ON digital_memories(category, status, updated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_memories_sync ON digital_memories(sync_status);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_sync_outbox_status ON digital_sync_outbox(status, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_sync_attempts_outbox ON digital_sync_attempts(outbox_id, finished_at);
|
|
`;
|
|
|
|
let digitalEmployeeSchemaEnsured = false;
|
|
|
|
export function initDatabase(): void {
|
|
try {
|
|
db = new Database(dbPath);
|
|
digitalEmployeeSchemaEnsured = false;
|
|
log.info('Database initialized at:', dbPath);
|
|
|
|
// Create tables
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
`);
|
|
ensureDigitalEmployeeSchema();
|
|
log.info('Database tables created');
|
|
} catch (error) {
|
|
log.error('Database initialization failed:', error);
|
|
}
|
|
}
|
|
|
|
export function ensureDigitalEmployeeSchema(): boolean {
|
|
if (!db) return false;
|
|
if (digitalEmployeeSchemaEnsured) return true;
|
|
try {
|
|
db.exec(DIGITAL_EMPLOYEE_SCHEMA_SQL);
|
|
digitalEmployeeSchemaEnsured = true;
|
|
return true;
|
|
} catch (error) {
|
|
log.error('Digital employee schema initialization failed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function getDb(): Database.Database | null {
|
|
return db;
|
|
}
|
|
|
|
export function closeDb(): void {
|
|
if (db) {
|
|
db.close();
|
|
db = null;
|
|
digitalEmployeeSchemaEnsured = false;
|
|
log.info('[App] Database closed');
|
|
}
|
|
}
|
|
|
|
export function readSetting(key: string): unknown {
|
|
const row = db?.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
|
|
if (!row?.value) return null;
|
|
try { return JSON.parse(row.value); } catch { return row.value; }
|
|
}
|
|
|
|
export function writeSetting(key: string, value: unknown): boolean {
|
|
if (!db) return false;
|
|
if (value === null || value === undefined) {
|
|
db.prepare('DELETE FROM settings WHERE key = ?').run(key);
|
|
} else {
|
|
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(key, JSON.stringify(value));
|
|
}
|
|
return true;
|
|
}
|