From c196a02c95abf0c19a96ebcfd07e9c4dc564561f Mon Sep 17 00:00:00 2001 From: Vaayne Date: Wed, 17 Sep 2025 14:59:08 +0800 Subject: [PATCH] feat: Implement database migration system and update agent/session handling --- package.json | 8 +- .../drizzle/0000_confused_wendigo.sql | 10 + .../database/drizzle/meta/0000_snapshot.json | 256 ++++++++++-------- .../database/drizzle/meta/_journal.json | 4 +- .../agents/database/MigrationService.ts | 45 ++- src/main/services/agents/drizzle.config.ts | 5 +- tests/apis/agents/agents.http | 7 +- tests/apis/agents/sessions.http | 4 +- 8 files changed, 192 insertions(+), 147 deletions(-) rename src/main/services/agents/database/drizzle/0000_dry_luke_cage.sql => resources/database/drizzle/0000_confused_wendigo.sql (84%) rename {src/main/services/agents => resources}/database/drizzle/meta/0000_snapshot.json (88%) rename {src/main/services/agents => resources}/database/drizzle/meta/_journal.json (67%) diff --git a/package.json b/package.json index 76df7f930d..41b243339d 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,10 @@ "release": "node scripts/version.js", "publish": "yarn build:check && yarn release patch push", "pulish:artifacts": "cd packages/artifacts && npm publish && cd -", - "agents:generate": "drizzle-kit generate --config src/main/services/agents/drizzle.config.ts", - "agents:push": "drizzle-kit push --config src/main/services/agents/drizzle.config.ts", - "agents:studio": "drizzle-kit studio --config src/main/services/agents/drizzle.config.ts", - "agents:drop": "drizzle-kit drop --config src/main/services/agents/drizzle.config.ts", + "agents:generate": "NODE_ENV='development' drizzle-kit generate --config src/main/services/agents/drizzle.config.ts", + "agents:push": "NODE_ENV='development' drizzle-kit push --config src/main/services/agents/drizzle.config.ts", + "agents:studio": "NODE_ENV='development' drizzle-kit studio --config src/main/services/agents/drizzle.config.ts", + "agents:drop": "NODE_ENV='development' drizzle-kit drop --config src/main/services/agents/drizzle.config.ts", "generate:icons": "electron-icon-builder --input=./build/logo.png --output=build", "analyze:renderer": "VISUALIZER_RENDERER=true yarn build", "analyze:main": "VISUALIZER_MAIN=true yarn build", diff --git a/src/main/services/agents/database/drizzle/0000_dry_luke_cage.sql b/resources/database/drizzle/0000_confused_wendigo.sql similarity index 84% rename from src/main/services/agents/database/drizzle/0000_dry_luke_cage.sql rename to resources/database/drizzle/0000_confused_wendigo.sql index 09f1c74c36..b2328e39c2 100644 --- a/src/main/services/agents/database/drizzle/0000_dry_luke_cage.sql +++ b/resources/database/drizzle/0000_confused_wendigo.sql @@ -1,3 +1,10 @@ +--> statement-breakpoint +CREATE TABLE `migrations` ( + `version` integer PRIMARY KEY NOT NULL, + `tag` text NOT NULL, + `executed_at` integer NOT NULL +); + CREATE TABLE `agents` ( `id` text PRIMARY KEY NOT NULL, `type` text NOT NULL, @@ -14,9 +21,11 @@ CREATE TABLE `agents` ( `created_at` text NOT NULL, `updated_at` text NOT NULL ); + --> statement-breakpoint CREATE TABLE `sessions` ( `id` text PRIMARY KEY NOT NULL, + `agent_type` text NOT NULL, `agent_id` text NOT NULL, `name` text NOT NULL, `description` text, @@ -31,6 +40,7 @@ CREATE TABLE `sessions` ( `created_at` text NOT NULL, `updated_at` text NOT NULL ); + --> statement-breakpoint CREATE TABLE `session_messages` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, diff --git a/src/main/services/agents/database/drizzle/meta/0000_snapshot.json b/resources/database/drizzle/meta/0000_snapshot.json similarity index 88% rename from src/main/services/agents/database/drizzle/meta/0000_snapshot.json rename to resources/database/drizzle/meta/0000_snapshot.json index 5ddcc15d4f..6bfe1204d7 100644 --- a/src/main/services/agents/database/drizzle/meta/0000_snapshot.json +++ b/resources/database/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "7ef99575-0fcf-471c-9da7-77e5cf8de6a2", + "id": "35efb412-0230-4767-9c76-7b7c4d40369f", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "agents": { @@ -112,114 +112,6 @@ "uniqueConstraints": {}, "checkConstraints": {} }, - "sessions": { - "name": "sessions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "agent_id": { - "name": "agent_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "accessible_paths": { - "name": "accessible_paths", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "instructions": { - "name": "instructions", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "model": { - "name": "model", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "plan_model": { - "name": "plan_model", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "small_model": { - "name": "small_model", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "mcps": { - "name": "mcps", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "allowed_tools": { - "name": "allowed_tools", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "configuration": { - "name": "configuration", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, "session_messages": { "name": "session_messages", "columns": { @@ -278,6 +170,152 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} + }, + "migrations": { + "name": "migrations", + "columns": { + "version": { + "name": "version", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessible_paths": { + "name": "accessible_paths", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_model": { + "name": "plan_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "small_model": { + "name": "small_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcps": { + "name": "mcps", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} } }, "views": {}, diff --git a/src/main/services/agents/database/drizzle/meta/_journal.json b/resources/database/drizzle/meta/_journal.json similarity index 67% rename from src/main/services/agents/database/drizzle/meta/_journal.json rename to resources/database/drizzle/meta/_journal.json index 0f87e9a936..86f2a531c0 100644 --- a/src/main/services/agents/database/drizzle/meta/_journal.json +++ b/resources/database/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1758035192486, - "tag": "0000_dry_luke_cage", + "when": 1758091173882, + "tag": "0000_confused_wendigo", "breakpoints": true } ] diff --git a/src/main/services/agents/database/MigrationService.ts b/src/main/services/agents/database/MigrationService.ts index 16e13f56ca..71d4b80c41 100644 --- a/src/main/services/agents/database/MigrationService.ts +++ b/src/main/services/agents/database/MigrationService.ts @@ -1,5 +1,6 @@ import { type Client } from '@libsql/client' import { loggerService } from '@logger' +import { getResourcePath } from '@main/utils' import { type LibSQLDatabase } from 'drizzle-orm/libsql' import fs from 'fs' import path from 'path' @@ -29,15 +30,18 @@ export class MigrationService { constructor(db: LibSQLDatabase, client: Client) { this.db = db this.client = client - this.migrationDir = path.join(__dirname, 'drizzle') + this.migrationDir = path.join(getResourcePath(), 'database', 'drizzle') } async runMigrations(): Promise { try { logger.info('Starting migration check...') - // Ensure migrations table exists - await this.ensureMigrationsTable() + const hasMigrationsTable = await this.migrationsTableExists() + + if (!hasMigrationsTable) { + logger.info('Migrations table not found; assuming fresh database state') + } // Read migration journal const journal = await this.readMigrationJournal() @@ -47,7 +51,9 @@ export class MigrationService { } // Get applied migrations - const appliedMigrations = await this.getAppliedMigrations() + const appliedMigrations = hasMigrationsTable + ? await this.getAppliedMigrations() + : [] const appliedVersions = new Set(appliedMigrations.map((m) => Number(m.version))) const latestAppliedVersion = appliedMigrations.reduce( @@ -82,27 +88,14 @@ export class MigrationService { } } - private async ensureMigrationsTable(): Promise { + private async migrationsTableExists(): Promise { try { - const tableExists = await this.client.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'`) - - if (tableExists.rows.length === 0) { - logger.info('Migrations table missing, creating...') - - await this.client.execute(` - CREATE TABLE IF NOT EXISTS migrations ( - version INTEGER PRIMARY KEY, - tag TEXT NOT NULL, - executed_at INTEGER NOT NULL - ) - `) - - logger.info('Migrations table created successfully') - } else { - logger.debug('Migrations table already exists') - } + const table = await this.client.execute( + `SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'` + ) + return table.rows.length > 0 } catch (error) { - logger.error('Failed to ensure migrations table exists:', { error }) + logger.error('Failed to check migrations table status:', { error }) throw error } } @@ -147,7 +140,7 @@ export class MigrationService { // Read and execute SQL const sqlContent = fs.readFileSync(sqlFilePath, 'utf-8') - await this.client.execute(sqlContent) + await this.client.executeMultiple(sqlContent) // Record migration as applied (store journal idx as version for tracking) const newMigration: NewMigration = { @@ -156,6 +149,10 @@ export class MigrationService { executedAt: Date.now() } + if (!(await this.migrationsTableExists())) { + throw new Error('Migrations table missing after executing migration; cannot record progress') + } + await this.db.insert(migrations).values(newMigration) const executionTime = Date.now() - startTime diff --git a/src/main/services/agents/drizzle.config.ts b/src/main/services/agents/drizzle.config.ts index 93752a2c01..e12518c069 100644 --- a/src/main/services/agents/drizzle.config.ts +++ b/src/main/services/agents/drizzle.config.ts @@ -5,12 +5,11 @@ import os from 'node:os' import path from 'node:path' -import { isDev } from '@main/constant' import { defineConfig } from 'drizzle-kit' import { app } from 'electron' function getDbPath() { - if (isDev) { + if (process.env.NODE_ENV === 'development') { return path.join(os.homedir(), '.cherrystudio', 'data', 'agents.db') } return path.join(app.getPath('userData'), 'agents.db') @@ -23,7 +22,7 @@ export const dbPath = resolvedDbPath export default defineConfig({ dialect: 'sqlite', schema: './src/main/services/agents/database/schema/index.ts', - out: './src/main/services/agents/database/drizzle', + out: './resources/database/drizzle', dbCredentials: { url: `file:${resolvedDbPath}` }, diff --git a/tests/apis/agents/agents.http b/tests/apis/agents/agents.http index a81b3ff970..ce21217ffd 100644 --- a/tests/apis/agents/agents.http +++ b/tests/apis/agents/agents.http @@ -1,5 +1,6 @@ @host=http://localhost:23333 @token=cs-sk-af798ed4-7cf5-4fd7-ae4b-df203b164194 +@agent_id=agent_1758092281575_tn9dxio9k ### List Agents GET {{host}}/v1/agents @@ -27,17 +28,17 @@ Content-Type: application/json } ### Get Agent Details -GET {{host}}/v1/agents/agent_1757663884173_4tyeh3vqq +GET {{host}}/v1/agents/{{agent_id}} Authorization: Bearer {{token}} Content-Type: application/json ### Delete Agent -DELETE {{host}}/v1/agents/agent_1757663884173 +DELETE {{host}}/v1/agents/{{agent_id}} Authorization: Bearer {{token}} Content-Type: application/json ### Update Agent -PATCH {{host}}/v1/agents/agent_1757663884173 +PATCH {{host}}/v1/agents/{{agent_id}} Authorization: Bearer {{token}} Content-Type: application/json diff --git a/tests/apis/agents/sessions.http b/tests/apis/agents/sessions.http index e5d026f30a..95660ae4eb 100644 --- a/tests/apis/agents/sessions.http +++ b/tests/apis/agents/sessions.http @@ -1,8 +1,8 @@ @host=http://localhost:23333 @token=cs-sk-af798ed4-7cf5-4fd7-ae4b-df203b164194 -@agent_id=agent_1758084953648_my4oxnbm3 -@session_id=session_1758087299124_2etavjo2x +@agent_id=agent_1758092281575_tn9dxio9k +@session_id=session_1758092305477_b0g0cmnkp ### List Sessions GET {{host}}/v1/agents/{{agent_id}}/sessions