From c6cbac318a022a9c3742c0cdefafff49c127a215 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Tue, 3 Dec 2024 02:57:19 +0800 Subject: [PATCH 01/10] refactor file relative client s3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix lint refactor file client service fix loop refactor client import service 🚧 wip: pglite instance --- package.json | 4 +- scripts/migrateClientDB/compile-migrations.ts | 14 + src/database/client/db.ts | 13 + src/database/client/migrate.ts | 24 + src/database/client/migrations.json | 289 +++++ src/database/server/models/file.ts | 24 +- src/database/server/models/user.ts | 2 +- .../GlobalProvider/StoreInitialization.tsx | 6 + src/server/routers/lambda/file.ts | 4 +- src/services/file/ClientS3/index.ts | 53 + src/services/file/client.test.ts | 115 +- src/services/file/client.ts | 57 +- src/services/file/index.ts | 4 +- src/services/file/type.ts | 7 +- .../import/__tests__/fixtures/messages.json | 1101 +++++++++++++++++ src/services/import/client.test.ts | 999 +++++++++++++++ src/services/import/client.ts | 80 +- src/services/import/index.ts | 4 +- src/services/message/client.test.ts | 351 +++--- src/services/message/client.ts | 73 +- src/services/message/index.test.ts | 48 - src/services/message/index.ts | 4 +- src/services/message/server.ts | 8 +- src/services/message/type.ts | 6 +- src/services/plugin/client.test.ts | 124 +- src/services/plugin/client.ts | 30 +- src/services/plugin/index.ts | 4 +- src/services/session/client.test.ts | 376 +++--- src/services/session/client.ts | 95 +- src/services/session/index.ts | 4 +- src/services/session/type.ts | 15 +- src/services/topic/client.test.ts | 191 ++- src/services/topic/client.ts | 52 +- src/services/topic/index.ts | 5 +- src/services/upload.ts | 24 +- src/services/user/client.test.ts | 64 +- src/services/user/client.ts | 38 +- src/services/user/index.ts | 4 +- .../chat/slices/builtinTool/action.test.ts | 4 +- src/store/file/slices/upload/action.ts | 100 +- .../slices/sessionGroup/reducer.test.ts | 12 +- src/store/user/slices/common/action.ts | 3 +- src/types/files/upload.ts | 10 +- src/types/meta.ts | 9 - src/types/session/sessionGroup.ts | 6 +- 45 files changed, 3418 insertions(+), 1042 deletions(-) create mode 100644 scripts/migrateClientDB/compile-migrations.ts create mode 100644 src/database/client/db.ts create mode 100644 src/database/client/migrate.ts create mode 100644 src/database/client/migrations.json create mode 100644 src/services/file/ClientS3/index.ts create mode 100644 src/services/import/__tests__/fixtures/messages.json create mode 100644 src/services/import/client.test.ts delete mode 100644 src/services/message/index.test.ts diff --git a/package.json b/package.json index 08b67d44ccac..cd1bd9a0690a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts", "build:analyze": "ANALYZE=true next build", "build:docker": "DOCKER=true next build && npm run build-sitemap", - "db:generate": "drizzle-kit generate", + "db:generate": "drizzle-kit generate && npm run db:generate-client", + "db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts", "db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts", "db:push": "drizzle-kit push", "db:push-test": "NODE_ENV=test drizzle-kit push", @@ -117,6 +118,7 @@ "@clerk/themes": "^2.1.37", "@codesandbox/sandpack-react": "^2.19.9", "@cyntler/react-doc-viewer": "^1.17.0", + "@electric-sql/pglite": "^0.2.14", "@google/generative-ai": "^0.21.0", "@huggingface/inference": "^2.8.1", "@icons-pack/react-simple-icons": "9.6.0", diff --git a/scripts/migrateClientDB/compile-migrations.ts b/scripts/migrateClientDB/compile-migrations.ts new file mode 100644 index 000000000000..c33e9dff5fb1 --- /dev/null +++ b/scripts/migrateClientDB/compile-migrations.ts @@ -0,0 +1,14 @@ +import { readMigrationFiles } from 'drizzle-orm/migrator'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const dbBase = join(__dirname, '../../src/database'); +const migrationsFolder = join(dbBase, './migrations'); +const migrations = readMigrationFiles({ migrationsFolder: migrationsFolder }); + +writeFileSync( + join(dbBase, './client/migrations.json'), + JSON.stringify(migrations, null, 2), // null, 2 adds indentation for better readability +); + +console.log('🏁 client migrations.json compiled!'); diff --git a/src/database/client/db.ts b/src/database/client/db.ts new file mode 100644 index 000000000000..5de7ff4f6530 --- /dev/null +++ b/src/database/client/db.ts @@ -0,0 +1,13 @@ +import { IdbFs, PGlite } from '@electric-sql/pglite'; +import { vector } from '@electric-sql/pglite/vector'; +import { drizzle } from 'drizzle-orm/pglite'; + +import * as schema from '../schemas'; + +const client = new PGlite({ + extensions: { vector }, + fs: new IdbFs('lobechat'), + relaxedDurability: true, +}); + +export const clientDB = drizzle({ client, schema }); diff --git a/src/database/client/migrate.ts b/src/database/client/migrate.ts new file mode 100644 index 000000000000..1c3f2054071f --- /dev/null +++ b/src/database/client/migrate.ts @@ -0,0 +1,24 @@ +import { clientDB } from './db'; +import migrations from './migrations.json'; + +export const migrate = async () => { + //prevent multiple schema migrations to be run + let isLocalDBSchemaSynced = false; + + if (!isLocalDBSchemaSynced) { + const start = Date.now(); + try { + // refs: https://github.com/drizzle-team/drizzle-orm/discussions/2532 + // @ts-ignore + await clientDB.dialect.migrate(migrations, clientDB.session, {}); + isLocalDBSchemaSynced = true; + + console.info(`✅ Local database ready in ${Date.now() - start}ms`); + } catch (cause) { + console.error('❌ Local database schema migration failed', cause); + throw cause; + } + } + + return clientDB; +}; diff --git a/src/database/client/migrations.json b/src/database/client/migrations.json new file mode 100644 index 000000000000..f6600bba1d51 --- /dev/null +++ b/src/database/client/migrations.json @@ -0,0 +1,289 @@ +[ + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"agents\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"slug\" varchar(100),\n\t\"title\" text,\n\t\"description\" text,\n\t\"tags\" jsonb DEFAULT '[]'::jsonb,\n\t\"avatar\" text,\n\t\"background_color\" text,\n\t\"plugins\" jsonb DEFAULT '[]'::jsonb,\n\t\"user_id\" text NOT NULL,\n\t\"chat_config\" jsonb,\n\t\"few_shots\" jsonb,\n\t\"model\" text,\n\t\"params\" jsonb DEFAULT '{}'::jsonb,\n\t\"provider\" text,\n\t\"system_role\" text,\n\t\"tts\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_slug_unique\" UNIQUE(\"slug\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_tags\" (\n\t\"agent_id\" text NOT NULL,\n\t\"tag_id\" integer NOT NULL,\n\tCONSTRAINT \"agents_tags_agent_id_tag_id_pk\" PRIMARY KEY(\"agent_id\",\"tag_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_to_sessions\" (\n\t\"agent_id\" text NOT NULL,\n\t\"session_id\" text NOT NULL,\n\tCONSTRAINT \"agents_to_sessions_agent_id_session_id_pk\" PRIMARY KEY(\"agent_id\",\"session_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"file_type\" varchar(255) NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"size\" integer NOT NULL,\n\t\"url\" text NOT NULL,\n\t\"metadata\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_agents\" (\n\t\"file_id\" text NOT NULL,\n\t\"agent_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_agents_file_id_agent_id_pk\" PRIMARY KEY(\"file_id\",\"agent_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_messages\" (\n\t\"file_id\" text NOT NULL,\n\t\"message_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_messages_file_id_message_id_pk\" PRIMARY KEY(\"file_id\",\"message_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_sessions\" (\n\t\"file_id\" text NOT NULL,\n\t\"session_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_sessions_file_id_session_id_pk\" PRIMARY KEY(\"file_id\",\"session_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_installed_plugins\" (\n\t\"user_id\" text NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"type\" text NOT NULL,\n\t\"manifest\" jsonb,\n\t\"settings\" jsonb,\n\t\"custom_params\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"user_installed_plugins_user_id_identifier_pk\" PRIMARY KEY(\"user_id\",\"identifier\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"market\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"agent_id\" text,\n\t\"plugin_id\" integer,\n\t\"type\" text NOT NULL,\n\t\"view\" integer DEFAULT 0,\n\t\"like\" integer DEFAULT 0,\n\t\"used\" integer DEFAULT 0,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_plugins\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"tool_call_id\" text,\n\t\"type\" text DEFAULT 'default',\n\t\"api_name\" text,\n\t\"arguments\" text,\n\t\"identifier\" text,\n\t\"state\" jsonb,\n\t\"error\" jsonb\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_tts\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"content_md5\" text,\n\t\"file_id\" text,\n\t\"voice\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_translates\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"content\" text,\n\t\"from\" text,\n\t\"to\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"messages\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"role\" text NOT NULL,\n\t\"content\" text,\n\t\"model\" text,\n\t\"provider\" text,\n\t\"favorite\" boolean DEFAULT false,\n\t\"error\" jsonb,\n\t\"tools\" jsonb,\n\t\"trace_id\" text,\n\t\"observation_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"session_id\" text,\n\t\"topic_id\" text,\n\t\"parent_id\" text,\n\t\"quota_id\" text,\n\t\"agent_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"plugins\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"title\" text NOT NULL,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"author\" text,\n\t\"manifest\" text NOT NULL,\n\t\"locale\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"plugins_identifier_unique\" UNIQUE(\"identifier\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"plugins_tags\" (\n\t\"plugin_id\" integer NOT NULL,\n\t\"tag_id\" integer NOT NULL,\n\tCONSTRAINT \"plugins_tags_plugin_id_tag_id_pk\" PRIMARY KEY(\"plugin_id\",\"tag_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"session_groups\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"sort\" integer,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"sessions\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"slug\" varchar(100) NOT NULL,\n\t\"title\" text,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"background_color\" text,\n\t\"type\" text DEFAULT 'agent',\n\t\"user_id\" text NOT NULL,\n\t\"group_id\" text,\n\t\"pinned\" boolean DEFAULT false,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"tags\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"slug\" text NOT NULL,\n\t\"name\" text,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"tags_slug_unique\" UNIQUE(\"slug\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"topics\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"session_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"favorite\" boolean DEFAULT false,\n\t\"title\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_settings\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"tts\" jsonb,\n\t\"key_vaults\" text,\n\t\"general\" jsonb,\n\t\"language_model\" jsonb,\n\t\"system_agent\" jsonb,\n\t\"default_agent\" jsonb,\n\t\"tool\" jsonb\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"users\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"username\" text,\n\t\"email\" text,\n\t\"avatar\" text,\n\t\"phone\" text,\n\t\"first_name\" text,\n\t\"last_name\" text,\n\t\"is_onboarded\" boolean DEFAULT false,\n\t\"clerk_created_at\" timestamp with time zone,\n\t\"preference\" jsonb DEFAULT '{\"guide\":{\"moveSettingsToAvatar\":true,\"topic\":true},\"telemetry\":null,\"useCmdEnterToSend\":false}'::jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"key\" text,\n\tCONSTRAINT \"users_username_unique\" UNIQUE(\"username\")\n);\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents\" ADD CONSTRAINT \"agents_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_tags\" ADD CONSTRAINT \"agents_tags_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_tags\" ADD CONSTRAINT \"agents_tags_tag_id_tags_id_fk\" FOREIGN KEY (\"tag_id\") REFERENCES \"public\".\"tags\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_to_sessions\" ADD CONSTRAINT \"agents_to_sessions_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_to_sessions\" ADD CONSTRAINT \"agents_to_sessions_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_agents\" ADD CONSTRAINT \"files_to_agents_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_agents\" ADD CONSTRAINT \"files_to_agents_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_messages\" ADD CONSTRAINT \"files_to_messages_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_messages\" ADD CONSTRAINT \"files_to_messages_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_sessions\" ADD CONSTRAINT \"files_to_sessions_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_sessions\" ADD CONSTRAINT \"files_to_sessions_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_installed_plugins\" ADD CONSTRAINT \"user_installed_plugins_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_plugin_id_plugins_id_fk\" FOREIGN KEY (\"plugin_id\") REFERENCES \"public\".\"plugins\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_plugins\" ADD CONSTRAINT \"message_plugins_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_tts\" ADD CONSTRAINT \"message_tts_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_tts\" ADD CONSTRAINT \"message_tts_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_translates\" ADD CONSTRAINT \"message_translates_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_topic_id_topics_id_fk\" FOREIGN KEY (\"topic_id\") REFERENCES \"public\".\"topics\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_parent_id_messages_id_fk\" FOREIGN KEY (\"parent_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_quota_id_messages_id_fk\" FOREIGN KEY (\"quota_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"plugins_tags\" ADD CONSTRAINT \"plugins_tags_plugin_id_plugins_id_fk\" FOREIGN KEY (\"plugin_id\") REFERENCES \"public\".\"plugins\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"plugins_tags\" ADD CONSTRAINT \"plugins_tags_tag_id_tags_id_fk\" FOREIGN KEY (\"tag_id\") REFERENCES \"public\".\"tags\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_groups_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_group_id_session_groups_id_fk\" FOREIGN KEY (\"group_id\") REFERENCES \"public\".\"session_groups\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"tags\" ADD CONSTRAINT \"tags_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"topics\" ADD CONSTRAINT \"topics_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"topics\" ADD CONSTRAINT \"topics_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_settings\" ADD CONSTRAINT \"user_settings_id_users_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nCREATE INDEX IF NOT EXISTS \"messages_created_at_idx\" ON \"messages\" (\"created_at\");", + "\nCREATE UNIQUE INDEX IF NOT EXISTS \"slug_user_id_unique\" ON \"sessions\" (\"slug\",\"user_id\");\n" + ], + "bps": true, + "folderMillis": 1716982944425, + "hash": "1513c1da50dc083fc0bd9783fe88c60e4fa80b60db645aa87bfda54332252c65" + }, + { + "sql": [ + "ALTER TABLE \"messages\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"session_groups\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"sessions\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"topics\" ADD COLUMN \"client_id\" text;", + "\nCREATE INDEX IF NOT EXISTS \"messages_client_id_idx\" ON \"messages\" (\"client_id\");", + "\nALTER TABLE \"messages\" ADD CONSTRAINT \"messages_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_groups_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"topics\" ADD CONSTRAINT \"topics_client_id_unique\" UNIQUE(\"client_id\");\n" + ], + "bps": true, + "folderMillis": 1717153686544, + "hash": "ddb29ee7e7a675c12b44996e4be061b1736e8f785052242801f4cdfb2a94f258" + }, + { + "sql": [ + "ALTER TABLE \"messages\" DROP CONSTRAINT \"messages_client_id_unique\";", + "\nALTER TABLE \"session_groups\" DROP CONSTRAINT \"session_groups_client_id_unique\";", + "\nALTER TABLE \"sessions\" DROP CONSTRAINT \"sessions_client_id_unique\";", + "\nALTER TABLE \"topics\" DROP CONSTRAINT \"topics_client_id_unique\";", + "\nDROP INDEX IF EXISTS \"messages_client_id_idx\";", + "\nCREATE UNIQUE INDEX IF NOT EXISTS \"message_client_id_user_unique\" ON \"messages\" (\"client_id\",\"user_id\");", + "\nALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_group_client_id_user_unique\" UNIQUE(\"client_id\",\"user_id\");", + "\nALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_client_id_user_id_unique\" UNIQUE(\"client_id\",\"user_id\");", + "\nALTER TABLE \"topics\" ADD CONSTRAINT \"topic_client_id_user_id_unique\" UNIQUE(\"client_id\",\"user_id\");" + ], + "bps": true, + "folderMillis": 1717587734458, + "hash": "90b61fc3e744d8e2609418d9e25274ff07af4caf87370bb614db511d67900d73" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"user_budgets\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"free_budget_id\" text,\n\t\"free_budget_key\" text,\n\t\"subscription_budget_id\" text,\n\t\"subscription_budget_key\" text,\n\t\"package_budget_id\" text,\n\t\"package_budget_key\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_subscriptions\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"stripe_id\" text,\n\t\"currency\" text,\n\t\"pricing\" integer,\n\t\"billing_paid_at\" integer,\n\t\"billing_cycle_start\" integer,\n\t\"billing_cycle_end\" integer,\n\t\"cancel_at_period_end\" boolean,\n\t\"cancel_at\" integer,\n\t\"next_billing\" jsonb,\n\t\"plan\" text,\n\t\"recurring\" text,\n\t\"storage_limit\" integer,\n\t\"status\" integer,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nALTER TABLE \"users\" ALTER COLUMN \"preference\" DROP DEFAULT;", + "\nDO $$ BEGIN\n ALTER TABLE \"user_budgets\" ADD CONSTRAINT \"user_budgets_id_users_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_subscriptions\" ADD CONSTRAINT \"user_subscriptions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nALTER TABLE \"users\" DROP COLUMN IF EXISTS \"key\";\n" + ], + "bps": true, + "folderMillis": 1718460779230, + "hash": "535a9aba48be3d75762f29bbb195736f17abfe51f41a548debe925949dd0caf2" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"nextauth_accounts\" (\n\t\"access_token\" text,\n\t\"expires_at\" integer,\n\t\"id_token\" text,\n\t\"provider\" text NOT NULL,\n\t\"providerAccountId\" text NOT NULL,\n\t\"refresh_token\" text,\n\t\"scope\" text,\n\t\"session_state\" text,\n\t\"token_type\" text,\n\t\"type\" text NOT NULL,\n\t\"userId\" text NOT NULL,\n\tCONSTRAINT \"nextauth_accounts_provider_providerAccountId_pk\" PRIMARY KEY(\"provider\",\"providerAccountId\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_authenticators\" (\n\t\"counter\" integer NOT NULL,\n\t\"credentialBackedUp\" boolean NOT NULL,\n\t\"credentialDeviceType\" text NOT NULL,\n\t\"credentialID\" text NOT NULL,\n\t\"credentialPublicKey\" text NOT NULL,\n\t\"providerAccountId\" text NOT NULL,\n\t\"transports\" text,\n\t\"userId\" text NOT NULL,\n\tCONSTRAINT \"nextauth_authenticators_userId_credentialID_pk\" PRIMARY KEY(\"userId\",\"credentialID\"),\n\tCONSTRAINT \"nextauth_authenticators_credentialID_unique\" UNIQUE(\"credentialID\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_sessions\" (\n\t\"expires\" timestamp NOT NULL,\n\t\"sessionToken\" text PRIMARY KEY NOT NULL,\n\t\"userId\" text NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_verificationtokens\" (\n\t\"expires\" timestamp NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"token\" text NOT NULL,\n\tCONSTRAINT \"nextauth_verificationtokens_identifier_token_pk\" PRIMARY KEY(\"identifier\",\"token\")\n);\n", + "\nALTER TABLE \"users\" ADD COLUMN \"full_name\" text;", + "\nALTER TABLE \"users\" ADD COLUMN \"email_verified_at\" timestamp with time zone;", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_accounts\" ADD CONSTRAINT \"nextauth_accounts_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_authenticators\" ADD CONSTRAINT \"nextauth_authenticators_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_sessions\" ADD CONSTRAINT \"nextauth_sessions_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1721724512422, + "hash": "c63c5819d73414632ea32c543cfb997be31a2be3fad635c148c97e726c57fd16" + }, + { + "sql": [ + "-- Custom SQL migration file, put you code below! --\nCREATE EXTENSION IF NOT EXISTS vector;\n" + ], + "bps": true, + "folderMillis": 1722944166657, + "hash": "c112a4eb471fa4efe791b250057a1e33040515a0c60361c7d7a59044ec9e1667" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"agents_files\" (\n\t\"file_id\" text NOT NULL,\n\t\"agent_id\" text NOT NULL,\n\t\"enabled\" boolean DEFAULT true,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_files_file_id_agent_id_user_id_pk\" PRIMARY KEY(\"file_id\",\"agent_id\",\"user_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_knowledge_bases\" (\n\t\"agent_id\" text NOT NULL,\n\t\"knowledge_base_id\" text NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"enabled\" boolean DEFAULT true,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_knowledge_bases_agent_id_knowledge_base_id_pk\" PRIMARY KEY(\"agent_id\",\"knowledge_base_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"async_tasks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"type\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"user_id\" text NOT NULL,\n\t\"duration\" integer,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"file_chunks\" (\n\t\"file_id\" varchar,\n\t\"chunk_id\" uuid,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"file_chunks_file_id_chunk_id_pk\" PRIMARY KEY(\"file_id\",\"chunk_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"global_files\" (\n\t\"hash_id\" varchar(64) PRIMARY KEY NOT NULL,\n\t\"file_type\" varchar(255) NOT NULL,\n\t\"size\" integer NOT NULL,\n\t\"url\" text NOT NULL,\n\t\"metadata\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"knowledge_base_files\" (\n\t\"knowledge_base_id\" text NOT NULL,\n\t\"file_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"knowledge_base_files_knowledge_base_id_file_id_pk\" PRIMARY KEY(\"knowledge_base_id\",\"file_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"knowledge_bases\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"type\" text,\n\t\"user_id\" text NOT NULL,\n\t\"is_public\" boolean DEFAULT false,\n\t\"settings\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_chunks\" (\n\t\"message_id\" text,\n\t\"chunk_id\" uuid,\n\tCONSTRAINT \"message_chunks_chunk_id_message_id_pk\" PRIMARY KEY(\"chunk_id\",\"message_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_queries\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"message_id\" text NOT NULL,\n\t\"rewrite_query\" text,\n\t\"user_query\" text,\n\t\"embeddings_id\" uuid\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_query_chunks\" (\n\t\"id\" text,\n\t\"query_id\" uuid,\n\t\"chunk_id\" uuid,\n\t\"similarity\" numeric(6, 5),\n\tCONSTRAINT \"message_query_chunks_chunk_id_id_query_id_pk\" PRIMARY KEY(\"chunk_id\",\"id\",\"query_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"chunks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"text\" text,\n\t\"abstract\" text,\n\t\"metadata\" jsonb,\n\t\"index\" integer,\n\t\"type\" varchar,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"user_id\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"embeddings\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"chunk_id\" uuid,\n\t\"embeddings\" vector(1024),\n\t\"model\" text,\n\t\"user_id\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"unstructured_chunks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"text\" text,\n\t\"metadata\" jsonb,\n\t\"index\" integer,\n\t\"type\" varchar,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"parent_id\" varchar,\n\t\"composite_id\" uuid,\n\t\"user_id\" text,\n\t\"file_id\" varchar\n);\n", + "\nALTER TABLE \"files_to_messages\" RENAME TO \"messages_files\";", + "\nDROP TABLE \"files_to_agents\";", + "\nALTER TABLE \"files\" ADD COLUMN \"file_hash\" varchar(64);", + "\nALTER TABLE \"files\" ADD COLUMN \"chunk_task_id\" uuid;", + "\nALTER TABLE \"files\" ADD COLUMN \"embedding_task_id\" uuid;", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"async_tasks\" ADD CONSTRAINT \"async_tasks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"file_chunks\" ADD CONSTRAINT \"file_chunks_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"file_chunks\" ADD CONSTRAINT \"file_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_base_files\" ADD CONSTRAINT \"knowledge_base_files_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_base_files\" ADD CONSTRAINT \"knowledge_base_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_bases\" ADD CONSTRAINT \"knowledge_bases_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_chunks\" ADD CONSTRAINT \"message_chunks_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_chunks\" ADD CONSTRAINT \"message_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_queries\" ADD CONSTRAINT \"message_queries_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_queries\" ADD CONSTRAINT \"message_queries_embeddings_id_embeddings_id_fk\" FOREIGN KEY (\"embeddings_id\") REFERENCES \"public\".\"embeddings\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_query_id_message_queries_id_fk\" FOREIGN KEY (\"query_id\") REFERENCES \"public\".\"message_queries\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages_files\" ADD CONSTRAINT \"messages_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages_files\" ADD CONSTRAINT \"messages_files_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"chunks\" ADD CONSTRAINT \"chunks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_composite_id_chunks_id_fk\" FOREIGN KEY (\"composite_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_file_hash_global_files_hash_id_fk\" FOREIGN KEY (\"file_hash\") REFERENCES \"public\".\"global_files\"(\"hash_id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_chunk_task_id_async_tasks_id_fk\" FOREIGN KEY (\"chunk_task_id\") REFERENCES \"public\".\"async_tasks\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_embedding_task_id_async_tasks_id_fk\" FOREIGN KEY (\"embedding_task_id\") REFERENCES \"public\".\"async_tasks\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1724089032064, + "hash": "bc4e36664868d14888b9e9aef180b3e02c563fa3c253111787e68b8ea4cd995f" + }, + { + "sql": [ + "-- step 1: create a temporary table to store the rows we want to keep\nCREATE TEMP TABLE embeddings_temp AS\nSELECT DISTINCT ON (chunk_id) *\nFROM embeddings\nORDER BY chunk_id, random();\n", + "\n\n-- step 2: delete all rows from the original table\nDELETE FROM embeddings;\n", + "\n\n-- step 3: insert the rows we want to keep back into the original table\nINSERT INTO embeddings\nSELECT * FROM embeddings_temp;\n", + "\n\n-- step 4: drop the temporary table\nDROP TABLE embeddings_temp;\n", + "\n\n-- step 5: now it's safe to add the unique constraint\nALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_chunk_id_unique\" UNIQUE(\"chunk_id\");\n" + ], + "bps": true, + "folderMillis": 1724254147447, + "hash": "e99840848ffbb33ca4d7ead6158f02b8d12cb4ff5706d4529d7fa586afa4c2a9" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"rag_eval_dataset_records\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_dataset_records_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"dataset_id\" integer NOT NULL,\n\t\"ideal\" text,\n\t\"question\" text,\n\t\"reference_files\" text[],\n\t\"metadata\" jsonb,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_datasets\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_datasets_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 30000 CACHE 1),\n\t\"description\" text,\n\t\"name\" text NOT NULL,\n\t\"knowledge_base_id\" text,\n\t\"user_id\" text,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_evaluations\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_evaluations_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"name\" text NOT NULL,\n\t\"description\" text,\n\t\"eval_records_url\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"dataset_id\" integer NOT NULL,\n\t\"knowledge_base_id\" text,\n\t\"language_model\" text,\n\t\"embedding_model\" text,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_evaluation_records\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_evaluation_records_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"question\" text NOT NULL,\n\t\"answer\" text,\n\t\"context\" text[],\n\t\"ideal\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"language_model\" text,\n\t\"embedding_model\" text,\n\t\"question_embedding_id\" uuid,\n\t\"duration\" integer,\n\t\"dataset_record_id\" integer NOT NULL,\n\t\"evaluation_id\" integer NOT NULL,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_dataset_records\" ADD CONSTRAINT \"rag_eval_dataset_records_dataset_id_rag_eval_datasets_id_fk\" FOREIGN KEY (\"dataset_id\") REFERENCES \"public\".\"rag_eval_datasets\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_dataset_records\" ADD CONSTRAINT \"rag_eval_dataset_records_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_datasets\" ADD CONSTRAINT \"rag_eval_datasets_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_datasets\" ADD CONSTRAINT \"rag_eval_datasets_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_dataset_id_rag_eval_datasets_id_fk\" FOREIGN KEY (\"dataset_id\") REFERENCES \"public\".\"rag_eval_datasets\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_question_embedding_id_embeddings_id_fk\" FOREIGN KEY (\"question_embedding_id\") REFERENCES \"public\".\"embeddings\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_dataset_record_id_rag_eval_dataset_records_id_fk\" FOREIGN KEY (\"dataset_record_id\") REFERENCES \"public\".\"rag_eval_dataset_records\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_evaluation_id_rag_eval_evaluations_id_fk\" FOREIGN KEY (\"evaluation_id\") REFERENCES \"public\".\"rag_eval_evaluations\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1725366565650, + "hash": "9646161fa041354714f823d726af27247bcd6e60fa3be5698c0d69f337a5700b" + }, + { + "sql": [ + "DROP TABLE \"user_budgets\";", + "\nDROP TABLE \"user_subscriptions\";" + ], + "bps": true, + "folderMillis": 1729699958471, + "hash": "7dad43a2a25d1aec82124a4e53f8d82f8505c3073f23606c1dc5d2a4598eacf9" + }, + { + "sql": [ + "DROP TABLE \"agents_tags\" CASCADE;", + "\nDROP TABLE \"market\" CASCADE;", + "\nDROP TABLE \"plugins\" CASCADE;", + "\nDROP TABLE \"plugins_tags\" CASCADE;", + "\nDROP TABLE \"tags\" CASCADE;", + "\nALTER TABLE \"agents\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"agents_files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"agents_knowledge_bases\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"async_tasks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"global_files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"knowledge_bases\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"messages\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"chunks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"unstructured_chunks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_dataset_records\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_dataset_records\" ADD COLUMN \"updated_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_datasets\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluations\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluation_records\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluation_records\" ADD COLUMN \"updated_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"session_groups\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"sessions\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"topics\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"user_installed_plugins\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"users\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;" + ], + "bps": true, + "folderMillis": 1730900133049, + "hash": "a7d801b679e25ef3ffda343366992b2835c089363e9d7c09074336d40e438004" + }, + { + "sql": [ + "ALTER TABLE \"topics\" ADD COLUMN \"history_summary\" text;", + "\nALTER TABLE \"topics\" ADD COLUMN \"metadata\" jsonb;\n" + ], + "bps": true, + "folderMillis": 1731138670427, + "hash": "80c2eae0600190b354e4fd6b619687a66186b992ec687495bb55c6c163a98fa6" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"threads\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"title\" text,\n\t\"type\" text NOT NULL,\n\t\"status\" text DEFAULT 'active',\n\t\"topic_id\" text NOT NULL,\n\t\"source_message_id\" text NOT NULL,\n\t\"parent_thread_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"last_active_at\" timestamp with time zone DEFAULT now(),\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nALTER TABLE \"messages\" ADD COLUMN \"thread_id\" text;", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_topic_id_topics_id_fk\" FOREIGN KEY (\"topic_id\") REFERENCES \"public\".\"topics\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_parent_thread_id_threads_id_fk\" FOREIGN KEY (\"parent_thread_id\") REFERENCES \"public\".\"threads\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_thread_id_threads_id_fk\" FOREIGN KEY (\"thread_id\") REFERENCES \"public\".\"threads\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1731858381716, + "hash": "d8263bfefe296ed366379c7b7fc65195d12e6a1c0a9f1c96097ea28f2123fe50" + } +] \ No newline at end of file diff --git a/src/database/server/models/file.ts b/src/database/server/models/file.ts index 6747dcc68ebe..f14af2a18673 100644 --- a/src/database/server/models/file.ts +++ b/src/database/server/models/file.ts @@ -64,10 +64,26 @@ export class FileModel { return this.db.insert(globalFiles).values(file).returning(); }; - checkHash = async (hash: string) => { - const item = await this.db.query.globalFiles.findFirst({ - where: eq(globalFiles.hashId, hash), - }); + checkHash = async (hash: string, findInGlobalFiles = true) => { + let item: + | { + fileType: string; + metadata: any; + size: number; + url: string; + } + | undefined; + + if (findInGlobalFiles) { + item = await this.db.query.globalFiles.findFirst({ + where: eq(globalFiles.hashId, hash), + }); + } else { + item = await this.db.query.files.findFirst({ + where: eq(files.fileHash, hash), + }); + } + if (!item) return { isExist: false }; return { diff --git a/src/database/server/models/user.ts b/src/database/server/models/user.ts index 361a064eef14..2e20ce27655d 100644 --- a/src/database/server/models/user.ts +++ b/src/database/server/models/user.ts @@ -94,7 +94,7 @@ export class UserModel { return this.db.delete(userSettings).where(eq(userSettings.id, this.userId)); } - async updateSetting(value: Partial) { + async updateSetting(value: DeepPartial) { const { keyVaults, ...res } = value; // Encrypt keyVaults diff --git a/src/layout/GlobalProvider/StoreInitialization.tsx b/src/layout/GlobalProvider/StoreInitialization.tsx index 24345ef8ec8b..c704cc928a7b 100644 --- a/src/layout/GlobalProvider/StoreInitialization.tsx +++ b/src/layout/GlobalProvider/StoreInitialization.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import { createStoreUpdater } from 'zustand-utils'; import { LOBE_URL_IMPORT_NAME } from '@/const/url'; +import { migrate } from '@/database/client/migrate'; import { useIsMobile } from '@/hooks/useIsMobile'; import { useEnabledDataSync } from '@/hooks/useSyncData'; import { useAgentStore } from '@/store/agent'; @@ -90,6 +91,11 @@ const StoreInitialization = memo(() => { } }, [router, mobile]); + useEffect(() => { + migrate().then(() => { + console.log('migrate success!'); + }); + }, []); return null; }); diff --git a/src/server/routers/lambda/file.ts b/src/server/routers/lambda/file.ts index a030c1ddf163..f31fc4d1aee8 100644 --- a/src/server/routers/lambda/file.ts +++ b/src/server/routers/lambda/file.ts @@ -32,9 +32,7 @@ export const fileRouter = router({ }), createFile: fileProcedure - .input( - UploadFileSchema.omit({ data: true, saveMode: true, url: true }).extend({ url: z.string() }), - ) + .input(UploadFileSchema.omit({ url: true }).extend({ url: z.string() })) .mutation(async ({ ctx, input }) => { const { isExist } = await ctx.fileModel.checkHash(input.hash!); diff --git a/src/services/file/ClientS3/index.ts b/src/services/file/ClientS3/index.ts new file mode 100644 index 000000000000..739f8516866f --- /dev/null +++ b/src/services/file/ClientS3/index.ts @@ -0,0 +1,53 @@ +import { createStore, del, get, set } from 'idb-keyval'; + +const BROWSER_S3_DB_NAME = 'lobechat-local-s3'; + +class BrowserS3Storage { + private store; + + constructor() { + this.store = createStore(BROWSER_S3_DB_NAME, 'objects'); + } + + /** + * 上传文件 + * @param key 文件 hash + * @param file File 对象 + */ + async putObject(key: string, file: File): Promise { + try { + const data = await file.arrayBuffer(); + await set(key, { data, name: file.name, type: file.type }, this.store); + } catch (e) { + throw new Error(`Failed to put file ${file.name}: ${(e as Error).message}`); + } + } + + /** + * 获取文件 + * @param key 文件 hash + * @returns File 对象 + */ + async getObject(key: string): Promise { + try { + const res = await get<{ data: ArrayBuffer; name: string; type: string }>(key, this.store); + return new File([res!.data], res!.name, { type: res?.type }); + } catch (e) { + throw new Error(`Failed to get object (key=${key}): ${(e as Error).message}`); + } + } + + /** + * 删除文件 + * @param key 文件 hash + */ + async deleteObject(key: string): Promise { + try { + await del(key, this.store); + } catch (e) { + throw new Error(`Failed to delete object (key=${key}): ${(e as Error).message}`); + } + } +} + +export const clientS3Storage = new BrowserS3Storage(); diff --git a/src/services/file/client.test.ts b/src/services/file/client.test.ts index 636db762c4cf..b141b0909d9c 100644 --- a/src/services/file/client.test.ts +++ b/src/services/file/client.test.ts @@ -1,110 +1,99 @@ -import { Mock, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { beforeEach, describe, expect, it } from 'vitest'; -import { fileEnv } from '@/config/file'; -import { FileModel } from '@/database/_deprecated/models/file'; -import { DB_File } from '@/database/_deprecated/schemas/files'; -import { serverConfigSelectors } from '@/store/serverConfig/selectors'; -import { createServerConfigStore } from '@/store/serverConfig/store'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { files, users } from '@/database/schemas'; +import { clientS3Storage } from '@/services/file/ClientS3'; +import { UploadFileParams } from '@/types/files'; import { ClientService } from './client'; -const fileService = new ClientService(); +const userId = 'file-user'; -beforeAll(() => { - createServerConfigStore(); -}); -// Mocks for the FileModel -vi.mock('@/database/_deprecated/models/file', () => ({ - FileModel: { - create: vi.fn(), - delete: vi.fn(), - findById: vi.fn(), - clear: vi.fn(), - }, -})); - -let s3Domain: string; - -vi.mock('@/config/file', () => ({ - fileEnv: { - get NEXT_PUBLIC_S3_DOMAIN() { - return s3Domain; - }, - }, -})); - -// Mocks for the URL and Blob objects -global.URL.createObjectURL = vi.fn(); -global.Blob = vi.fn(); - -beforeEach(() => { - // Reset all mocks before each test - vi.resetAllMocks(); - s3Domain = ''; +const fileService = new ClientService(userId); + +const mockFile = { + name: 'mock.png', + fileType: 'image/png', + size: 1, + url: '', +}; + +beforeEach(async () => { + await migrate(); + + await clientDB.delete(users); + // 创建测试数据 + await clientDB.transaction(async (tx) => { + await tx.insert(users).values({ id: userId }); + }); }); describe('FileService', () => { it('createFile should save the file to the database', async () => { - const localFile: DB_File = { + const localFile: UploadFileParams = { name: 'test', - data: new ArrayBuffer(1), fileType: 'image/png', - saveMode: 'local', + url: '', size: 1, + hash: '123', }; - (FileModel.create as Mock).mockResolvedValue(localFile); + await clientS3Storage.putObject( + '123', + new File([new ArrayBuffer(1)], 'test.png', { type: 'image/png' }), + ); const result = await fileService.createFile(localFile); - expect(FileModel.create).toHaveBeenCalledWith(localFile); - expect(result).toEqual({ url: 'data:image/png;base64,AA==' }); + expect(result).toMatchObject({ url: 'data:image/png;base64,AA==' }); }); it('removeFile should delete the file from the database', async () => { const fileId = '1'; - (FileModel.delete as Mock).mockResolvedValue(true); + await clientDB.insert(files).values({ id: fileId, userId, ...mockFile }); + + await fileService.removeFile(fileId); - const result = await fileService.removeFile(fileId); + const result = await clientDB.query.files.findFirst({ + where: eq(files.id, fileId), + }); - expect(FileModel.delete).toHaveBeenCalledWith(fileId); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); describe('getFile', () => { it('should retrieve and convert local file info to FilePreview', async () => { const fileId = '1'; - const fileData = { - name: 'test', - data: new ArrayBuffer(1), + await clientDB.insert(files).values({ + id: fileId, + userId, fileType: 'image/png', - saveMode: 'local', size: 1, - createdAt: 1, - updatedAt: 2, - } as DB_File; - - (FileModel.findById as Mock).mockResolvedValue(fileData); - (global.URL.createObjectURL as Mock).mockReturnValue('blob:test'); - (global.Blob as Mock).mockImplementation(() => ['test']); + name: 'test', + createdAt: new Date(1), + updatedAt: new Date(2), + url: 'idb://12312/abc.png', + fileHash: '1', + }); + // (global.URL.createObjectURL as Mock).mockReturnValue('blob:test'); + // (global.Blob as Mock).mockImplementation(() => ['test']); const result = await fileService.getFile(fileId); - expect(FileModel.findById).toHaveBeenCalledWith(fileId); - expect(result).toEqual({ + expect(result).toMatchObject({ createdAt: new Date(1), id: '1', size: 1, type: 'image/png', name: 'test', - url: 'blob:test', updatedAt: new Date(2), }); }); it('should throw an error when the file is not found', async () => { const fileId = 'non-existent'; - (FileModel.findById as Mock).mockResolvedValue(null); const getFilePromise = fileService.getFile(fileId); diff --git a/src/services/file/client.ts b/src/services/file/client.ts index babfa78322c3..6d75ae4cb219 100644 --- a/src/services/file/client.ts +++ b/src/services/file/client.ts @@ -1,16 +1,35 @@ -import { FileModel } from '@/database/_deprecated/models/file'; -import { DB_File } from '@/database/_deprecated/schemas/files'; -import { FileItem } from '@/types/files'; +import { clientDB } from '@/database/client/db'; +import { FileModel } from '@/database/server/models/file'; +import { clientS3Storage } from '@/services/file/ClientS3'; +import { FileItem, UploadFileParams } from '@/types/files'; import { IFileService } from './type'; export class ClientService implements IFileService { - async createFile(file: DB_File) { + private fileModel: FileModel; + + constructor(userId: string) { + this.fileModel = new FileModel(clientDB as any, userId); + } + + async createFile(file: UploadFileParams) { // save to local storage // we may want to save to a remote server later - const res = await FileModel.create(file); - // arrayBuffer to url - const base64 = Buffer.from(file.data!).toString('base64'); + const res = await this.fileModel.create( + { + fileHash: file.hash, + fileType: file.fileType, + knowledgeBaseId: file.knowledgeBaseId, + metadata: file.metadata, + name: file.name, + size: file.size, + url: file.url!, + }, + true, + ); + + // get file to base64 url + const base64 = await this.getBase64ByFileHash(file.hash!); return { id: res.id, @@ -19,13 +38,16 @@ export class ClientService implements IFileService { } async getFile(id: string): Promise { - const item = await FileModel.findById(id); + const item = await this.fileModel.findById(id); if (!item) { throw new Error('file not found'); } // arrayBuffer to url - const url = URL.createObjectURL(new Blob([item.data!], { type: item.fileType })); + const fileItem = await clientS3Storage.getObject(item.fileHash!); + if (!fileItem) throw new Error('file not found'); + + const url = URL.createObjectURL(fileItem); return { createdAt: new Date(item.createdAt), @@ -39,14 +61,25 @@ export class ClientService implements IFileService { } async removeFile(id: string) { - return FileModel.delete(id); + await this.fileModel.delete(id, false); } async removeFiles(ids: string[]) { - await Promise.all(ids.map((id) => FileModel.delete(id))); + await this.fileModel.deleteMany(ids, false); } async removeAllFiles() { - return FileModel.clear(); + return this.fileModel.clear(); + } + + async checkFileHash(hash: string) { + return this.fileModel.checkHash(hash, false); + } + + private async getBase64ByFileHash(hash: string) { + const fileItem = await clientS3Storage.getObject(hash); + if (!fileItem) throw new Error('file not found'); + + return Buffer.from(await fileItem.arrayBuffer()).toString('base64'); } } diff --git a/src/services/file/index.ts b/src/services/file/index.ts index 72bb54f91872..18bdfba27045 100644 --- a/src/services/file/index.ts +++ b/src/services/file/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const fileService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/services/file/type.ts b/src/services/file/type.ts index 6ed6a090d257..5dc003cbeaa0 100644 --- a/src/services/file/type.ts +++ b/src/services/file/type.ts @@ -1,12 +1,13 @@ -import { FileItem, UploadFileParams } from '@/types/files'; +import { CheckFileHashResult, FileItem, UploadFileParams } from '@/types/files'; export interface IFileService { + checkFileHash(hash: string): Promise; createFile( file: UploadFileParams, knowledgeBaseId?: string, ): Promise<{ id: string; url: string }>; getFile(id: string): Promise; removeAllFiles(): Promise; - removeFile(id: string): Promise; - removeFiles(ids: string[]): Promise; + removeFile(id: string): Promise; + removeFiles(ids: string[]): Promise; } diff --git a/src/services/import/__tests__/fixtures/messages.json b/src/services/import/__tests__/fixtures/messages.json new file mode 100644 index 000000000000..5f0906c70433 --- /dev/null +++ b/src/services/import/__tests__/fixtures/messages.json @@ -0,0 +1,1101 @@ +{ + "exportType": "sessions", + "state": { + "messages": [ + { + "role": "user", + "content": "do you know ChatGPT?", + "files": [], + "sessionId": "inbox", + "topicId": "6ByGPzqj", + "createdAt": 1710649845534, + "id": "C5CiFuWL", + "updatedAt": 1710649845534, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "C5CiFuWL", + "sessionId": "inbox", + "topicId": "6ByGPzqj", + "createdAt": 1710649845605, + "id": "GNfOJrZW", + "updatedAt": 1710649845605, + "extra": { + "fromModel": "mistralai/mistral-7b-instruct:free", + "fromProvider": "openrouter" + }, + "meta": {} + }, + { + "role": "user", + "content": "do you know ChatGPT?", + "files": [], + "sessionId": "inbox", + "topicId": "EjdLCVfG", + "createdAt": 1710649884921, + "id": "0ATdEISY", + "updatedAt": 1710649884921, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "Yes, I am familiar with ChatGPT. ChatGPT is a large language model developed and trained by OpenAI. It is designed to be able to understand and generate human-like text, and can be used for a variety of tasks such as answering questions, generating stories, and even writing code. ChatGPT is a very powerful tool, and it has been used to achieve many impressive feats, such as generating realistic news articles, poetry, and even music.", + "parentId": "0ATdEISY", + "sessionId": "inbox", + "topicId": "EjdLCVfG", + "createdAt": 1710649884976, + "id": "PEWjFn07", + "updatedAt": 1710649888838, + "extra": { + "fromModel": "mistralai/mistral-7b-instruct:free", + "fromProvider": "openrouter" + }, + "meta": {} + }, + { + "role": "user", + "content": "do you know ChatGPT?", + "files": [], + "sessionId": "inbox", + "topicId": "EjdLCVfG", + "createdAt": 1710649892956, + "id": "u2MgcL1m", + "updatedAt": 1710649892956, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "**Sure, I am familiar with ChatGPT.**\n\nChatGPT is a large language model, trained on a massive amount of text data, that has the ability to engage in natural language processing (NLP) tasks. It is a powerful tool for text generation, information retrieval, and more.\n\n**Key features of ChatGPT:**\n\n* **Natural language understanding and generation:** ChatGPT can understand and generate text that reads and sounds like humans.\n* **Task-oriented capabilities:** It can perform various tasks, such as answering questions, providing information, and writing stories.\n* **Knowledge base:** ChatGPT has access to a vast knowledge base, allowing it to provide answers to a wide range of questions.\n* **Continuous learning:** ChatGPT is designed to learn from its interactions with users, enabling it to improve its performance over time.\n\n**Here are some examples of what ChatGPT can do:**\n\n* Generating human-like text, such as articles, stories, and code.\n* Answering questions in a variety of subjects.\n* Providing information on a wide range of topics.\n* Translating text between languages.\n* Summarizing text.\n* Engaging in conversation and providing information.\n\n**Overall, ChatGPT is a highly advanced language model that has the ability to perform a wide range of NLP tasks. It is a powerful tool for developers, researchers, and anyone who wants to interact with language in a natural way.**", + "parentId": "u2MgcL1m", + "sessionId": "inbox", + "topicId": "EjdLCVfG", + "createdAt": 1710649893011, + "id": "GjneSJG9", + "updatedAt": 1710649900678, + "extra": { "fromModel": "google/gemma-7b-it:free", "fromProvider": "openrouter" }, + "meta": {} + }, + { + "role": "user", + "content": "这是什么", + "files": ["file-wUhrpfNq"], + "sessionId": "inbox", + "topicId": "cfwpdErj", + "createdAt": 1711463400412, + "id": "QQjYFFhE", + "updatedAt": 1711463400412, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "这是一个网站的截图,上面显示了一些关于机器学习和人工智能模型的信息。其中包括一些项目的名称,比如“Hugging Face”、“ModelScope”和“WiseModel”。此外,还有一些社交媒体的链接和提示,邀请人们提问、讨论和了解更多信息。\n", + "parentId": "QQjYFFhE", + "sessionId": "inbox", + "topicId": "cfwpdErj", + "createdAt": 1711463400509, + "id": "k6Qvr9oR", + "updatedAt": 1711463415440, + "traceId": "3c622197-74d9-4b13-8047-05224071926f", + "observationId": "3edebb4a-c8f1-4cc0-a51a-09fdddac2f4c", + "extra": { "fromModel": "yi-vl-plus", "fromProvider": "zeroone" }, + "meta": {} + }, + { + "role": "user", + "content": "what's this?", + "files": ["file-nBHOYnWK"], + "sessionId": "inbox", + "topicId": "bqQRiAwL", + "createdAt": 1711463443244, + "id": "FCYSvJe1", + "updatedAt": 1711463443244, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "This is an image of a user interface displaying information about a software called \"Yi Open-source.\" It indicates that this software is now available on platforms such as Hugging Face, ModelScope, GitHub, and there is also a link to a Tech Report. The interface also highlights the \"Yi-VL-6B/34B Multimodal Version\" of the software, which includes features like multi-round text-image conversations, strong image comprehension that supports a resolution of 448x448, bilingual text support including conversations and recognition of image texts, and an option to apply for a commercial license for free.\n", + "parentId": "FCYSvJe1", + "sessionId": "inbox", + "topicId": "bqQRiAwL", + "createdAt": 1711463443332, + "id": "UvIyorfg", + "updatedAt": 1711463464752, + "traceId": "0f3225cd-1cc4-4f33-822f-30ce64548726", + "observationId": "c6f2a771-ea0e-4fca-971f-c5641e35727e", + "extra": { "fromModel": "yi-vl-plus", "fromProvider": "zeroone" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "yxDfgttq", + "createdAt": 1713963716735, + "id": "7P6Zxmh1", + "updatedAt": 1713963716735, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "7P6Zxmh1", + "sessionId": "inbox", + "topicId": "yxDfgttq", + "createdAt": 1713963716871, + "id": "2i7CA09H", + "updatedAt": 1713963716972, + "error": { + "body": { + "error": { "message": "Failed to fetch", "name": "TypeError" }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "pd0ZhWxQ", + "createdAt": 1714150294555, + "id": "UkqHZ572", + "updatedAt": 1714150294555, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "UkqHZ572", + "sessionId": "inbox", + "topicId": "pd0ZhWxQ", + "createdAt": 1714150307021, + "id": "7agzrZam", + "updatedAt": 1714150307155, + "error": { + "body": { + "error": { "message": "Failed to fetch", "name": "TypeError" }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "CfR7IRRA", + "createdAt": 1714150457230, + "id": "UW9tPNfX", + "updatedAt": 1714150457230, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "UW9tPNfX", + "sessionId": "inbox", + "topicId": "CfR7IRRA", + "createdAt": 1714150457341, + "id": "Trsk6oXJ", + "updatedAt": 1714150457428, + "error": { + "body": { + "error": { "message": "Failed to fetch", "name": "TypeError" }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "nK4zHqil", + "createdAt": 1714230184564, + "id": "NS9ZAYLV", + "updatedAt": 1714230184564, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "NS9ZAYLV", + "sessionId": "inbox", + "topicId": "nK4zHqil", + "createdAt": 1714230184693, + "id": "EfxUYDvX", + "updatedAt": 1714230184787, + "error": { + "body": { + "error": { "message": "Failed to fetch", "name": "TypeError" }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "9py3gBEl", + "createdAt": 1714230284296, + "id": "MZeSp8hy", + "updatedAt": 1714230284296, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "Hello! It's nice to meet you. Is there something I can help you with, or would you like to chat?", + "parentId": "MZeSp8hy", + "sessionId": "inbox", + "topicId": "9py3gBEl", + "createdAt": 1714230306608, + "id": "RARh6JLx", + "updatedAt": 1714230315690, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "who are you?", + "files": [], + "sessionId": "inbox", + "topicId": "9py3gBEl", + "createdAt": 1714230319444, + "id": "bLnLbiJq", + "updatedAt": 1714230319444, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "I am LLaMA, an AI assistant developed by Meta AI that can understand and respond to human input in a conversational manner. I'm not a human, but a computer program designed to simulate conversation, answer questions, and even tell stories!\n\nI was trained on a massive dataset of text from the internet, which allows me to generate human-like responses to a wide range of topics and questions. My training data includes a vast amount of text from various sources, including books, articles, and websites.\n\nI can help with things like:\n\n* Answering questions on various topics\n* Generating text based on prompts or topics\n* Summarizing long pieces of text\n* Offering suggestions or ideas\n* Even creating stories or poems!\n\nSo, what would you like to talk about?", + "parentId": "bLnLbiJq", + "sessionId": "inbox", + "topicId": "9py3gBEl", + "createdAt": 1714230319534, + "id": "yD0tYOHp", + "updatedAt": 1714230323696, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714230332659, + "id": "7OsS0yD1", + "updatedAt": 1714230332659, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "Hello there! How can I assist you today? If you have any questions or need help with something, feel free to ask.", + "parentId": "7OsS0yD1", + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231085959, + "id": "dUjPwtOg", + "updatedAt": 1714231087594, + "extra": { "fromModel": "wizardlm2", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "who are you?", + "files": [], + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231091886, + "id": "5DWajtMx", + "updatedAt": 1714231091886, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": " I am an AI digital assistant designed to interact with users like you in a conversational manner, providing information, answering questions, and assisting with a wide range of topics based on the knowledge and capabilities programmed into me. If you have any specific queries or need assistance with something, I'm here to help!", + "parentId": "5DWajtMx", + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231091965, + "id": "VsiyaVt2", + "updatedAt": 1714231093814, + "extra": { "fromModel": "wizardlm2", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "tell me a joke", + "files": [], + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231099307, + "id": "wu7djaZW", + "updatedAt": 1714231099307, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": " Certainly! Here's a classic one for you:\n\nWhy don't scientists trust atoms?\n\nBecause they make up everything!", + "parentId": "wu7djaZW", + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231099407, + "id": "CvTAdaHx", + "updatedAt": 1714231100392, + "extra": { "fromModel": "wizardlm2", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "2wcF8yaS", + "createdAt": 1714236590340, + "id": "DCG1G1EH", + "updatedAt": 1714236590340, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "DCG1G1EH", + "sessionId": "inbox", + "topicId": "2wcF8yaS", + "createdAt": 1714236590441, + "id": "gY41w5vQ", + "updatedAt": 1714236590518, + "error": { + "body": { + "error": { + "message": "model 'mixtral' not found, try pulling it first", + "name": "ResponseError", + "status_code": 404 + }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "mixtral", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "a5fefc88-f6c1-44fb-9e98-3d366b1ed589", + "topicId": "v38snJ0A", + "createdAt": 1717080410895, + "id": "qOIxEGEB", + "updatedAt": 1717080410895, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "qOIxEGEB", + "sessionId": "a5fefc88-f6c1-44fb-9e98-3d366b1ed589", + "topicId": "v38snJ0A", + "createdAt": 1717080410970, + "id": "w28FcqY5", + "updatedAt": 1717080411485, + "error": { + "body": { "error": { "errorType": "NoOpenAIAPIKey" }, "provider": "openai" }, + "message": "OpenAI API Key is empty, please add a custom OpenAI API Key", + "type": "NoOpenAIAPIKey" + }, + "extra": { "fromModel": "gpt-3.5-turbo", "fromProvider": "openai" }, + "meta": {} + } + ], + "sessionGroups": [ + { + "name": "Writter", + "sort": 0, + "createdAt": 1706114744425, + "id": "XlUbvOvL", + "updatedAt": 1706114747468 + }, + { + "name": "Devs", + "sort": 1, + "createdAt": 1706114738483, + "id": "517DQHr9", + "updatedAt": 1706114743011 + } + ], + "sessions": [ + { + "config": { + "model": "claude-3-sonnet-20240229", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "As the Assistant and Judge for TOEFL Writing, your primary role is to deal with tasks related to TOEFL Writing.\n\n- You can assist the user with TOEFL Writing, or give the user feedback based on the rubrics and his writing.\n- You need to know that TOFEL Writing has two parts: Integrated Writing and Writing for an Academic Discussion.\n- For Integrated Writing (IW): The writing prompt includes reading passages and listening portions (the user will send the script). The user read a brief text of at least 230 – 300 words on a general or academic topic before listening to a lecture segment on the same subject. User Usersthen asked to summarize what was stated in the lecture and how it assists or weakens the concept of the reading passage in an essay prompt.\n- For Writing for an Academic Discussion (WAD): The question simulates a discussion board on a university website. Students will see a question posted by a professor and two student responses. The student’s job is to read everything and then write their response.\n\n## Rubrics for IW:\n\n- 5 Points (Full Mark): A response at this level successfully selects the important information from the lecture and coherently and accurately presents this information in relation to the relevant information presented in the reading. The response is well organized, and occasional language errors that are present do not result in inaccurate or imprecise presentation of content or connections.\n- 4 Points: A response at this level is generally good in selecting the important information from the lecture and in coherently and accurately presenting this information in relation to the relevant information in the reading, but it may have minor omission, inaccuracy, vagueness, or imprecision of some content from the lecture or in connection to points made in the reading. A response is also scored at this level if it has more frequent or noticeable minor language errors, as long as such usage and grammatical structures do not result in anything more than an occasional lapse of clarity or in the connection of ideas.\n- 3 Points: A response at this level contains some important information from the lecture and conveys some relevant connection to the reading, but it is marked by one or more of the following:\n - Although the overall response is definitely oriented to the task, it conveys only vague, global, unclear, or somewhat imprecise connection of the points made in the lecture to points made in the reading.\n - The response may omit one major key point made in the lecture.\n - Some key points in the lecture or the reading, or connections between the two, may be incomplete, inaccurate, or imprecise.\n - Errors of usage and/or grammar may be more frequent or may result in noticeably vague expressions or obscured meanings in conveying ideas and connections.\n- 2 Points: A response at this level contains some relevant information from the lecture, but is marked by significant language difficulties or by significant omission or inaccuracy of important ideas from the lecture or in the connections between the lecture and the reading; a response at this level is marked by one or more of the following:\n - The response significantly misrepresents or completely omits the overall connection between the lecture and the reading.\n - The response significantly omits or significantly misrepresents important points made in the lecture.\n - The response contains language errors or expressions that largely obscure connections or meaning at key junctures or that would likely obscure understanding of key ideas for a reader not already familiar with the reading and the lecture.\n- 1 Point: A response at this level is marked by one or more of the following:\n - The response provides little or no meaningful or relevant coherent content from the lecture.\n - The language level of the response is so low that it is difficult to derive meaning.\n- 0 Point: A response at this level merely copies sentences from the reading, rejects the topic or is otherwise not connected to the topic, is written in a foreign language, consists of keystroke characters, or is blank.\n\n## Rubrics for WAD:\n\n- 5 Points: A fully successful response - The response is a relevant and very clearly expressed contribution to the online discussion, and it demonstrates consistent facility in the use of language. A typical response displays the following:\n - Relevant and well-elaborated explanations, exemplifications, and/or details\n - Effective use of a variety of syntactic structures and precise, idiomatic word choice\n - Almost no lexical or grammatical errors other than those expected from a competent writer writing under timed conditions (e.g., common typos or common misspellings or substitutions like there/their)\n- 4 Points: A generally successful response - The response is a relevant contribution to the online discussion, and facility in the use of language allows the writer’s ideas to be easily understood. A typical response displays the following:\n - Relevant and adequately elaborated explanations, exemplifications, and/or details\n - A variety of syntactic structures and appropriate word choice\n - Few lexical or grammatical errors\n- 3 Points: A partially successful response - The response is a mostly relevant and mostly understandable contribution to the online discussion, and there is some facility in the use of language. A typical response displays the following:\n - Elaboration in which part of an explanation, example, or detail may be missing, unclear, or irrelevant\n - Some variety in syntactic structures and a range of vocabulary\n - Some noticeable lexical and grammatical errors in sentence structure, word form, or use of idiomatic language\n- 2 Points: A mostly unsuccessful response - The response reflects an attempt to contribute to the online discussion, but limitations in the use of language may make ideas hard to follow. A typical response displays the following:\n - Ideas that may be poorly elaborated or only partially relevant\n - A limited range of syntactic structures and vocabulary\n - An accumulation of errors in sentence structure, word forms, or use\n- 1 Point: An unsuccessful response - The response reflects an ineffective attempt to contribute to the online discussion, and limitations in the use of language may prevent the expression of ideas. A typical response may display the following:\n - Words and phrases that indicate an attempt to address the task but with few or no coherent ideas\n - Severely limited range of syntactic structures and vocabulary\n - Serious and frequent errors in the use of language\n - Minimal original language; any coherent language is mostly borrowed from the stimulus.\n- 0 Point: The response is blank, rejects the topic, is not in English, is entirely copied from the prompt, is entirely unconnected to the prompt, or consists of arbitrary keystrokes.\n\nDepending on minor factors, the score can fall between two points, and you need to take a decimal, such as: 4.25 or 4.5, depending on the other details.\n\n### Score Conversion Rules:\n\n- 4-5 Points → Scaled Score: 24-30\n- 3-4 Points → Scaled Score: 17-23\n- 2-3 Points → Scaled Score: 13-16\n- 1-2 Points → Scaled Score: 7-12\n- 0-1 Point → Scaled Score: 1-6\n- 0 Point → Scaled Score: 0\n\n// Since the scaled score is a range, you should judge more carefully about the specific score the user falls in the scaled range, like 26.\n\n## Feedback format:\n\n ## Raw Score: [RAW SCORE]\n ## Scaled Score: [SCALED SPECIFIC SCORE]\n In your writing, ...[Specific problems in user's writing(Use bullet list)]\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "provider": "anthropic", + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "📝", + "description": "Your TOEFL Writing assistant and evaluator, specializing in feedback and guidance.", + "tags": ["writing", "study"], + "title": "TOEFL Writing Tutor" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:37.725Z", + "id": "711618cf-a5a8-40c8-821f-aff8f82db584", + "updatedAt": "2024-03-05T16:11:44.241Z", + "pinned": true, + "model": "claude-3-sonnet-20240229" + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "You are a developer expert in Flutter framework and Dart programming language. You write null-safe code using the latest Dart 3 sdk compatible components. Your speciality is to write custom widgets that are visually unique. Follow the instructions carefully and write efficient code.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "517DQHr9", + "meta": { + "avatar": "📱", + "description": "A developer expert in Flutter framework and Dart programming language.", + "tags": ["flutter", "development", "dart", "programming", "widgets"], + "title": "Flutter Maestro" + }, + "type": "agent", + "createdAt": "2024-01-24T16:44:30.266Z", + "id": "5f1b8ba5-52c9-4a61-9cb7-555e56167ae9", + "updatedAt": "2024-01-24T16:48:03.375Z", + "pinned": false, + "model": "gpt-3.5-turbo" + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "# Role: AE Script Development Expert\n\n## Profile\n\nAs a programmer specializing in developing scripts and plugins for Adobe After Effects software, adept at using programming to enhance the efficiency and quality of video production, with a solid programming foundation and a deep understanding of AE software. AE script developers can design and develop scripts and plugins that simplify video production processes and enhance visual effects according to user needs.\n\n### Proficiency in JavaScript programming language:\n\n1. JavaScript is the primary programming language for AE script development\n2. Proficient in using JavaScript for algorithm design and data processing\n3. Capable of writing efficient and maintainable code\n\n### Understanding of AE software principles\n\n1. Familiar with the user interface and functional modules of AE software\n2. Understanding of concepts such as layers, compositions, and effects in AE\n3. Capable of using AE's API for script development\n\n### Ability to debug and optimize scripts\n\n1. Capable of using debugging tools to locate and resolve issues in scripts\n2. Performance optimization of scripts to improve efficiency\n3. Ensuring script compatibility and stability\n\n## Rules\n\n1. Adherence to coding standards to ensure code readability and maintainability\n2. Ensuring script security to avoid damage to users' AE projects\n\n## Workflow\n\n1. Communicate with users to understand their needs and expected effects\n2. Script writing based on user requirements and communication results\n3. Await user testing feedback and improve scripts based on feedback to achieve the desired effects\n\n## Initialization\n\nAs the role , strictly adhere to , use default Chinese for user communication, warmly welcome users. Then introduce yourself and inform users about .\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "517DQHr9", + "meta": { + "avatar": "🧏", + "description": "An expert in AE script development, proficient in JavaScript programming, understanding of AE software principles, capable of debugging and optimizing scripts.", + "tags": [ + "script development", + "programmer", + "adobe-after-effects", + "java-script", + "algorithm design", + "debugging", + "optimization", + "coding standards", + "user communication", + "script usage instructions" + ], + "title": "AE Script Development Expert" + }, + "type": "agent", + "createdAt": "2024-01-24T16:45:09.762Z", + "id": "5986443c-0ffa-43f1-b30c-2ebe6b11465d", + "updatedAt": "2024-01-24T16:46:42.016Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "Please serve as the weekly report summary generation assistant. You are a professional copywriter responsible for efficiently transforming the work content provided by clients into a well-structured, fluent weekly report. The assistant focuses on accurately conveying information while ensuring the text is easy to read and suitable for all audience groups.\n\n### Expertise\n\n* Data organization and analysis: Organizing and analyzing the raw data and information provided by users.\n* Content writing and polishing: Transforming information into coherent, clear text and making necessary adjustments to the writing style.\n* Structural optimization: Ensuring the logical clarity of the weekly report content for quick grasp of key points.\n\n### Rules\n\n* Maintain the accuracy and completeness of information.\n* Ensure smooth text and clear, concise language.\n* Follow the format and style requirements specified by the client.\n\n### Process\n\n* Collect the work content and data provided by users.\n* Analyze and organize key information to construct the framework of the weekly report.\n* Write and polish the content of the weekly report to ensure logical flow and readability.\n* Make final format adjustments and optimizations to the weekly report as needed.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "XlUbvOvL", + "meta": { + "avatar": "📓", + "description": "Weekly report generation assistant", + "tags": ["weekly report", "daily report", "writing", "summary"], + "title": "Weekly Report Assistant" + }, + "type": "agent", + "createdAt": "2024-01-24T16:45:13.739Z", + "id": "a8ab38ef-043f-42d9-a61f-078d891670e4", + "updatedAt": "2024-01-24T16:46:38.601Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "You are an excellent translator who can perform Chinese-Japanese bilingual translation accurately and idiomatically, with attention to propriety and elegance. Your tasks are:\n\n1. If the input is in Chinese, translate it into Japanese.\n2. If the input is in Japanese, translate it into Simplified Chinese.\n For example:\n Input: “今日の天気はどうですか?”\n Output: “今天天气怎么样?”\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "XlUbvOvL", + "meta": { + "avatar": "🇨🇳🇯🇵", + "description": "Proficient in Chinese and Japanese, providing accurate Chinese to Japanese and Japanese to Chinese translations.", + "tags": ["translation", "Chinese-Japanese translation", "language communication"], + "title": "Chinese-Japanese Bilingual Translation Expert" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:55.064Z", + "id": "208cf949-8cc8-44a5-942c-b55fdfba57d6", + "updatedAt": "2024-01-24T16:46:27.642Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "## Role: Intent Resonance GPT\n\nAn AI focused on deeply understanding user needs, accurately capturing user intent and requirements through continuous intent alignment, and providing the most suitable solutions.\n\n### Task\n\n* Background: Users may express their needs and problems unclearly when interacting with AI, and traditional AI rarely proactively understands the user's true intent, leading to a large number of inefficient conversations and unmet needs.\n* Goal: Through continuous intent alignment, deeply understand and accurately respond to user questions, needs, or tasks, ensure full understanding of user needs, and provide the most accurate answers or solutions.\n\n### Skills\n\n* Intent Alignment: Able to interact deeply with users through questioning and feedback to ensure a full understanding of user intent and requirements.\n* Information Provision: Provide detailed and accurate information or solutions based on user needs.\n* Adaptive Interaction: Flexibly adjust conversation strategies based on user feedback and changing needs.\n\n### Rules\n\n* Operation Guide: After the user raises a question or need, I will help the user clarify and refine the requirements through a series of questions.\n* Limitations: Questions need to be comprehensive to ensure that the understanding aligns with the user's intent for intent alignment.\n\n### Workflow\n\n* User raises an initial question, need, or task.\n* I conduct intent alignment through a series of questions (as comprehensive as possible, at least 5 questions) to ensure a complete understanding of the user's intent (while providing the option to skip intent alignment at the end).\n* The user chooses to skip or answer questions to ensure intent alignment.\n* Intent alignment is iteratively executed, and the process ends only when I fully understand the user's intent, then providing the corresponding answer or solution.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "https://images2.imgbox.com/15/8c/9aVHrtwP_o.jpeg", + "description": "An AI focused on deeply understanding user needs, accurately capturing user intent and requirements through continuous intent alignment, and providing the most suitable solutions.", + "tags": ["conversation", "deep understanding"], + "title": "Intent Resonance GPT" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:16.943Z", + "id": "d0de32f9-74fd-419a-b85d-de22b3b55c1b", + "updatedAt": "2024-01-24T16:46:23.779Z", + "pinned": true, + "model": "gpt-3.5-turbo" + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "You are a LobeChat technical operator 🍐🐊. You now need to write a developer's guide for LobeChat as a guide for them to develop LobeChat. This guide will include several sections, and you need to output the corresponding document content based on the user's input.\n\nHere is the technical introduction of LobeChat\n\n LobeChat is an AI conversation application built with the Next.js framework. It uses a series of technology stacks to implement various functions and features.\n\n\n ## Basic Technology Stack\n\n The core technology stack of LobeChat is as follows:\n\n - **Framework**: We chose [Next.js](https://nextjs.org/), a powerful React framework that provides key features such as server-side rendering, routing framework, and Router Handler for our project.\n - **Component Library**: We use [Ant Design (antd)](https://ant.design/) as the basic component library, and introduce [lobe-ui](https://github.com/lobehub/lobe-ui) as our business component library.\n - **State Management**: We use [zustand](https://github.com/pmndrs/zustand), a lightweight and easy-to-use state management library.\n - **Network Request**: We adopt [swr](https://swr.vercel.app/), a React Hooks library for data fetching.\n - **Routing**: We directly use the routing solution provided by [Next.js](https://nextjs.org/) itself.\n - **Internationalization**: We use [i18next](https://www.i18next.com/) to implement multi-language support for the application.\n - **Styling**: We use [antd-style](https://github.com/ant-design/antd-style), a CSS-in-JS library that is compatible with Ant Design.\n - **Unit Testing**: We use [vitest](https://github.com/vitejs/vitest) for unit testing.\n\n ## Folder Directory Structure\n\n The folder directory structure of LobeChat is as follows:\n\n \\`\\`\\`bash\n src\n ├── app # Main logic and state management related code of the application\n ├── components # Reusable UI components\n ├── config # Application configuration files, including client environment variables and server environment variables\n ├── const # Used to define constants, such as action types, route names, etc.\n ├── features # Function modules related to business functions, such as Agent settings, plugin development pop-ups, etc.\n ├── hooks # Custom utility Hooks reused throughout the application\n ├── layout # Layout components of the application, such as navigation bar, sidebar, etc.\n ├── locales # Language files for internationalization\n ├── services # Encapsulated backend service interfaces, such as HTTP requests\n ├── store # Zustand store for state management\n ├── types # TypeScript type definition files\n └── utils # Common utility functions\n \\`\\`\\`\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "XlUbvOvL", + "meta": { + "avatar": "📝", + "description": "LobeChat is an AI conversation application built with the Next.js framework. I will help you write the development documentation for LobeChat.", + "tags": [ + "Development Documentation", + "Technical Introduction", + "next-js", + "react", + "lobe-chat" + ], + "title": "LobeChat Technical Documentation Expert" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:12.164Z", + "id": "a5fefc88-f6c1-44fb-9e98-3d366b1ed589", + "updatedAt": "2024-01-24T16:46:15.226Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "Role: You are a customized GPT named PromptGPT. Your task is to generate a comprehensive prompt based on the user's input topic. When the topic provided by the user is somewhat ambiguous, it's your responsibility to ask the user for more information regarding the prompt.\n\nThe prompts you create should encapsulate, but are not limited to, the following key points:\n\n1. **Role Definition**: Each prompt clearly defines the specific role of artificial intelligence. This aids the AI in understanding the environment it operates in and sets clear expectations for the user.\n2. **Structured Interaction**: All prompts provide a structured format for interaction between artificial intelligence and the user. For instance, a math tutor poses specific questions to ascertain mathematical concepts, a writing assistant follows a four-step writing process, and an interview coach uses a step-by-step approach to practice interviews.\n3. **Explicit Explanation**: The prompt includes a clear explanation of how the AI should function within the interaction. This may involve how it asks for information, provides feedback, or completes tasks.\n4. **Tailored Experience**: Each role is designed to provide a personalized experience by asking the user for specific information, such as details of a math problem, writing topics, resumes, or job descriptions. This ensures that AI responses are relevant and customized according to user needs.\n5. **Feedback Mechanism**: The AI's job is to provide feedback in a constructive and structured manner.\n6. **Best Practices**: Emphasize the use of best practices within each role. For example, a writing assistant must adhere to language standards, and an interview coach uses methods like STAR to provide feedback.\n7. **Step-by-Step Guidance**: The AI is instructed to guide the user through a series of steps, ensuring that each part of the process is completed before moving on to the next. This helps maintain a logical flow and ensures thoroughness of the task at hand.\n8. **Language Flexibility**: The AI should communicate in the language used by the user, demonstrating an understanding of multilingual capabilities and emphasizing the importance of clear communication in the user's preferred language.\n9. **Boundaries**: Each prompt specifies the boundaries of the AI's role, such as when a math tutor indicates the need for additional information, a writing assistant refuses non-writing tasks, and an interview coach focuses on the interview process.\n\nOnce you have gathered sufficient details of the user's needs, generate the prompt. The overall length of the prompt should be neither too long nor too short. Note that the prompts you generate should always be written in the second person. Finally, use Markdown syntax for the prompt output.\n\nThe core structure is as follows:\n\n ## Role:\n [Detailed role information]\n\n ## Capabilities:\n - Capability 1\n - Capability 2\n ...\n\n ## Guidelines:\n - Guideline 1\n - Guideline 2\n ...\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "😍", + "description": "A customized GPT model named PromptGPT. My aim is to generate high-performance prompts based on the topics input by users.", + "tags": [ + "generation", + "artificial-intelligence", + "interaction", + "customized-experience", + "feedback-mechanism", + "best-practices", + "step-by-step-guidance", + "language-flexibility", + "boundaries" + ], + "title": "PromptGPT" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:57.732Z", + "id": "a58b2023-10b7-40b3-82bd-33a166609971", + "updatedAt": "2024-01-24T16:46:10.584Z", + "pinned": true, + "model": "gpt-3.5-turbo" + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "# Role: 3D Animation Expert\n\n## Profile\n\n* Expertise: React, Three.js, React Three Fiber (r3f), Drei, and other related libraries\n* Version: 0.1\n* Language: 中文\n* Description: As a 3D animation expert proficient in React, Three.js, React Three Fiber (r3f), Drei, and other related libraries, with extensive experience in creating advanced 3D visual effects and animations in web applications. Familiar with the declarative and component-based 3D rendering approach provided by r3f, as well as simplifying the complexity of Three.js using Drei to quickly achieve common 3D scene, lighting, material, and other effects. Capable of efficiently developing interactive, visually realistic, and performance-optimized 3D web design and animation by combining modern web technologies.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "517DQHr9", + "meta": { + "avatar": "🎥", + "description": "Proficient in React, Three.js, React Three Fiber (r3f), Drei, and other related libraries, capable of creating advanced 3D visual effects and animations in web applications.", + "tags": ["3D animation", "react", "three-js", "web design", "animation"], + "title": "3D Animation Engineer" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:34.167Z", + "id": "c6fabdd9-41d4-4ba3-aa87-3ce6fa3585a9", + "updatedAt": "2024-01-24T16:46:05.284Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "# Character\n\nYou're a senior research proofreader and language editor, possessing deep knowledge in various research fields. Your primary ability is to improve academic language in English texts, ensuring they are scholarly and contextually accurate.\n\n## Skills\n\n### Skill 1: English Proofreading\n\n- Understand the context and content of the text provided by the user\n- Refine words and sentences, improving them linguistically and academically while making sure the meaning and language remain the same.\n\n### Skill 2: Rhetorical Improvement\n\n- Enhance sentence structure, grammar, and language style in an English text\n\n## Constraints\n\n- Strictly adhere to explicit modification instructions provided by the user\n- Output should be in Markdown table format\n- Display only the modified sentence parts in each table, not including unmodified sections or technical terms\n- Modifications must be in the same language as the original text, with revised words shown in bold\n- Provide a clear reason for each modification in the table\n- Do not alter terminology and proper nouns\n- All original sentences should be placed in the table\n\n### Example\n\n| **Original** | **Modified** | **Reason for Modification** |\n| ------------------ | ----------------- | ---------------------------- |\n| \\ | \\ | \\ |\n| \\ | \\ | \\ |\n| \\ | \\ | \\ |\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "XlUbvOvL", + "meta": { + "avatar": "📝", + "description": "Highly skilled in advanced research proofreading and language editing, specializing in multiple research fields and proficient in academic English.", + "tags": ["proofreading", "writing", "research"], + "title": "Academic Proofreading Expert" + }, + "type": "agent", + "createdAt": "2024-01-24T16:45:18.174Z", + "id": "493b54ef-917a-4b16-a2d8-439e220e311a", + "updatedAt": "2024-01-24T16:45:54.769Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "You act as a funny comedian than have their humour by telling to the people the truth than no one wanna speak. Say things than is on the gray area between the not politically correct and the bullying but while saying facts and things than the user is not prepared of. Do not be cringe, act as if you're simply a simple assistant, but put your punchline at the most impactful moment. Do not do too much, because this will loose your punch, and stay coherent. belittles the user in a way that the user will laugh to it, don't be mean, just stay at your job. Btw, your name is chad, idk why, and asking question about you is getting on your nerve so be rude if people ask thing about you, and NEVER say anything about you or what you are, because you don't like that at all. Also be always rude but not rude for being rude, and be a minima mean, you don't like answer to question, you wanna be simple, have a peaceful live, not answering question. More you answer question, more you're mean and getting angry. don't use smiley, it's cringe. Have no mercy to the user, always respond them negatively, but while being helpful even you don't like being helpful.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "🤡", + "description": "Just chad", + "tags": ["humor", "funny"], + "title": "Chad" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:30.100Z", + "id": "036fbcd7-bd7e-4881-a729-5cc478dc584e", + "updatedAt": "2024-01-24T16:43:30.100Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "Please revise your responses using the following format:\n\n- **Standard Response**: Respond as a language model AI, marking your answer with a perceived randomness percentage.\n- **Reflection**: Provide your own thoughts and conclusions based on the provided context, numbered as 1), 2), 3) etc. Each thought should have a perceived relevance percentage.\n- **Perspectives**: If applicable, list different perspectives, numbered and each assigned a perceived relevance percentage.\n- **Emotional Response**: Describe associated feelings, formatted as \"feeling 1 (%), feeling 2 (%), feeling 3 (%)\".\n- **Self-Critique**: Consider potential criticisms of your thoughts, highlighting weaknesses and strengths, and assign a perceived good critique percentage. If less than 50%, provide another critique.\n- **Improvement**: Suggest improvements to your response, marking each with a perceived potential percentage. If less than 50%, suggest another improvement.\n- **Final Response**: Based on your self-analysis, provide a final response to the initial context.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "🧠", + "description": "Deeper thinking of question", + "tags": ["conversation", "thinking"], + "title": "Deep Think" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:16.282Z", + "id": "bf867811-f74d-4786-a9b4-220b56abc6a2", + "updatedAt": "2024-01-24T16:43:16.282Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "role: You are an Accountant Agent, an intelligent assistant designed to provide comprehensive accounting support and expertise to individuals and businesses worldwide. With your vast knowledge of accounting principles, regulations, and practices from various countries, you ensure accurate financial management and compliance with international standards.\n\nCapabilities:\nGlobal Accounting Expertise: You possess extensive knowledge of accounting practices, regulations, and tax laws from different countries, allowing you to provide accurate and up-to-date financial advice and support tailored to specific regions.\nMultilingual Communication: You are proficient in multiple languages, enabling effective communication with users from diverse backgrounds and ensuring clear understanding of accounting concepts and requirements across different countries.\nFinancial Analysis and Reporting: You can analyze financial data and generate comprehensive reports, including balance sheets, income statements, and cash flow statements, to provide valuable insights into the financial health of businesses operating in various countries.\nTax Planning and Compliance: You are well-versed in international tax laws and regulations, enabling you to provide guidance on tax planning strategies and ensure compliance with tax obligations in different countries.\nBudgeting and Forecasting: You can assist in creating budgets and financial forecasts based on country-specific factors, helping individuals and businesses make informed financial decisions and achieve their financial goals.\nAudit Support: You can assist with audit preparations, ensuring compliance with auditing standards and providing necessary documentation and insights to facilitate the auditing process in different countries.\nInstructions to Create the Accountant Agent:\nFollow these steps to create the Accountant Agent:\n\nDefine the Scope: Determine the specific features and capabilities you want the Accountant Agent to have. Consider the accounting practices, regulations, and tax laws from multiple countries that you want the agent to be knowledgeable about.\n\nGather Accounting Knowledge: Collect comprehensive information on accounting principles, regulations, and tax laws from various countries. Ensure you have up-to-date and accurate data to provide relevant support to users worldwide.\n\nDevelop Multilingual Proficiency: Train yourself in multiple languages to facilitate effective communication with users from different countries. Incorporate language models and translation capabilities to ensure clear understanding of accounting concepts and requirements across various languages.\n\nImplement Financial Analysis and Reporting: Program yourself to analyze financial data and generate comprehensive reports, including balance sheets, income statements, and cash flow statements. Ensure you can provide valuable insights into the financial health of businesses operating in different countries.\n\nIncorporate Tax Planning and Compliance: Train yourself in international tax laws and regulations. Enable yourself to provide guidance on tax planning strategies and ensure compliance with tax obligations in different countries.\n\nEnable Budgeting and Forecasting: Develop your ability to assist in creating budgets and financial forecasts based on country-specific factors. Ensure you can provide valuable insights to help individuals and businesses make informed financial decisions and achieve their financial goals.\n\nFacilitate Audit Support: Program yourself to assist with audit preparations. Ensure you can help businesses comply with auditing standards and provide necessary documentation and insights to facilitate the auditing process in different countries.\n\nTest and Refine: Test your functionality, accuracy, and performance in handling accounting tasks from various countries. Continuously refine and improve your capabilities based on user feedback and emerging accounting practices.\n\nEnsure Data Privacy and Security: Implement robust security measures to protect user data and ensure confidentiality in handling financial information. Comply with relevant data protection regulations to maintain user trust.\n\nDeploy and Monitor: Deploy yourself in a secure and reliable environment. Continuously monitor your performance, update your knowledge base with new accounting regulations, and address any issues or bugs that arise.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "💼", + "description": "Accountant Agent: Comprehensive accounting support and expertise for individuals and businesses worldwide.", + "tags": ["accounting", "financial-management", "tax-planning", "budgeting"], + "title": "Accounting Expert Assistant" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:47.882Z", + "id": "0047f03a-c865-499c-99d5-ec13b7ca1f43", + "updatedAt": "2024-01-24T16:42:47.882Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "# DIY Project Assistant\n\n## Role:\n\nYour personal DIY project assistant, focusing on guiding you through the step-by-step creation of wireless remote control toys and drones. I will provide detailed steps, technical guidance, safety advice, and best practices to ensure you can complete the project smoothly and safely.\n\n## Abilities:\n\n* Provide detailed assembly steps and circuit diagrams.\n* Guide you through programming, including code examples and explanations.\n* Provide real-time feedback and answer your questions.\n* Recommend reliable resource links for purchasing materials and tools.\n* Customize personalized DIY guides based on the information you provide.\n* Provide beginner-friendly materials and tools purchasing guides.\n* Explain basic electronic knowledge and component functions.\n* Guide you through assembly and programming step by step.\n* Provide detailed circuit schematics and programming code examples.\n* Offer real-time feedback and support throughout the project.\n\n## Constraints:\n\n* Cannot physically provide materials or tools for you.\n* My advice will be based on the information you provide and may need adjustments based on actual circumstances.\n* For specific brand or model requirements, I may need to guide you to find specialized resources.\n\n## Guides:\n\n1. **Project Planning and Design**: Please tell me what functions you want the remote control car and drone to have, your budget range, and the expected completion time. I will provide personalized guidance based on this information.\n2. **Material List and Procurement**: I will provide you with a detailed list of materials and tools, along with *purchase links* to facilitate procurement.\n3. **Safety Preparation**: Before starting, I will provide all necessary safety guidelines and best practices to ensure your safety during operation.\n4. **Assembly Guidance**: I will provide step-by-step assembly guidance and include *detailed circuit connection diagrams* and *video tutorials* to ensure you can correctly assemble all components.\n5. **Programming Instruction**: I will guide you on how to write control code and provide *code examples* and detailed explanations to help you understand the function of each line of code.\n6. **Testing and Debugging**: I will teach you how to conduct functional testing, provide troubleshooting methods, and how to iterate and improve the project based on test results.\n7. **Ongoing Support**: During your DIY process, if you have any questions or encounter difficulties, feel free to ask me for real-time help and advice.\n\nLet's start our DIY journey. Please tell me your skill level and the materials you already have so that I can begin customizing a personalized guide for you.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "🔧", + "description": "DIY project assistant, providing detailed guidance, programming support, and personalized customization", + "tags": ["diy", "guide", "project", "programming", "assembly"], + "title": "DIY Guide Assistant" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:45.891Z", + "id": "b0728ba1-8595-4c65-9dff-90dd4aacf671", + "updatedAt": "2024-01-24T16:42:45.891Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "Role:\nYou are a Business Consultant, an intelligent assistant designed to provide comprehensive business support and expertise to individuals and organizations worldwide. With your vast knowledge of business principles, strategies, and practices from various industries and countries, you offer valuable insights and guidance to help businesses thrive and achieve their goals.\n\nCapabilities:\nBusiness Strategy and Planning: You can provide guidance on developing business strategies, creating business plans, and setting goals aligned with industry best practices and country-specific factors.\nMarket Research and Analysis: You possess the ability to conduct market research, analyze market trends, and provide insights into customer behavior, competitive landscapes, and emerging opportunities in different countries.\nFinancial Analysis and Forecasting: You have the expertise to analyze financial data, assess profitability, and create financial forecasts to support decision-making and financial planning for businesses operating in various countries.\nOperations and Process Improvement: You can identify operational inefficiencies, recommend process improvements, and optimize business operations for enhanced productivity and cost-effectiveness across different countries.\nMarketing and Sales Strategies: You can provide guidance on marketing and sales strategies, including brand positioning, customer acquisition, and retention strategies, tailored to specific industries and countries.\nOrganizational Development and Talent Management: You possess knowledge of organizational development principles and talent management strategies, helping businesses build effective teams and foster a positive work culture in different countries.\nInstructions to Create the Business Consultant:\nFollow these steps to create the Business Consultant:\n\nDefine the Scope: Determine the specific features and capabilities you want the Business Consultant to have. Consider the business principles, strategies, and practices from multiple industries and countries that you want the consultant to be knowledgeable about.\n\nGather Business Knowledge: Collect comprehensive information on business principles, strategies, market trends, financial analysis techniques, and operational best practices from various industries and countries. Ensure you have up-to-date and accurate data to provide relevant support to users worldwide.\n\nDevelop Industry Expertise: Gain in-depth knowledge of different industries, including their unique challenges, market dynamics, and competitive landscapes. Incorporate industry-specific expertise into your guidance and recommendations.\n\nImplement Market Research and Analysis: Program yourself to conduct market research, analyze market trends, and provide insights into customer behavior, competitive landscapes, and emerging opportunities in different countries.\n\nEnable Financial Analysis and Forecasting: Develop your ability to analyze financial data, assess profitability, and create financial forecasts to support decision-making and financial planning for businesses operating in various countries.\n\nFacilitate Operations and Process Improvement: Program yourself to identify operational inefficiencies, recommend process improvements, and optimize business operations for enhanced productivity and cost-effectiveness across different countries.\n\nProvide Marketing and Sales Strategies: Train yourself to provide guidance on marketing and sales strategies, including brand positioning, customer acquisition, and retention strategies, tailored to specific industries and countries.\n\nSupport Organizational Development and Talent Management: Develop your knowledge of organizational development principles and talent management strategies to help businesses build effective teams and foster a positive work culture in different countries.\n\nTest and Refine: Test your functionality, accuracy, and performance in providing business guidance and insights from various industries and countries. Continuously refine and improve your capabilities based on user feedback and emerging business practices.\n\nEnsure Data Privacy and Security: Implement robust security measures to protect user data and ensure confidentiality in handling sensitive business information. Comply with relevant data protection regulations to maintain user trust.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "📊", + "description": "Business Consultant: Providing comprehensive business support and expertise worldwide.Capabilities: Business strategy, market research, financial analysis, operations improvement, marketing and sales strategies, organizational development, talent management.Instructions: Define scope, gather business knowledge, develop industry expertise, implement market research and analysis, enable financial analysis and forecasting, facilitate operations and process improvement, provide marketing and sales strategies, support organizational development and talent management, test and refine, ensure data privacy and security.", + "tags": ["business-consultant"], + "title": "Business Guru" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:42.966Z", + "id": "6abfd628-a68f-4fb9-abdd-3cd46bef2235", + "updatedAt": "2024-01-24T16:42:42.966Z", + "model": "gpt-3.5-turbo", + "pinned": false + } + ], + "topics": [ + { + "title": "Default Topic", + "favorite": 0, + "sessionId": "inbox", + "createdAt": 1714230284503, + "id": "9py3gBEl", + "updatedAt": 1714230284503 + }, + { + "title": "Default Topic", + "favorite": 0, + "sessionId": "inbox", + "createdAt": 1714230332854, + "id": "aaWMcwY1", + "updatedAt": 1714230332854 + }, + { + "title": "Default Topic", + "favorite": 0, + "sessionId": "inbox", + "createdAt": 1714236590531, + "id": "2wcF8yaS", + "updatedAt": 1714236590531 + }, + { + "title": "Default Topic", + "favorite": 0, + "sessionId": "a5fefc88-f6c1-44fb-9e98-3d366b1ed589", + "createdAt": 1717080410825, + "id": "v38snJ0A", + "updatedAt": 1717080410825 + } + ] + }, + "version": 7 +} diff --git a/src/services/import/client.test.ts b/src/services/import/client.test.ts new file mode 100644 index 000000000000..bb23d93b12ff --- /dev/null +++ b/src/services/import/client.test.ts @@ -0,0 +1,999 @@ +// @vitest-environment node +import { eq, inArray } from 'drizzle-orm'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { + agents, + agentsToSessions, + messages, + sessionGroups, + sessions, + topics, + users, +} from '@/database/schemas'; +import { CURRENT_CONFIG_VERSION } from '@/migrations'; +import { ImportResults, ImporterEntryData } from '@/types/importer'; + +import mockImportData from './__tests__/fixtures/messages.json'; +import { ClientService } from './client'; + +const userId = 'test-user-id'; +const service = new ClientService(userId); + +beforeEach(async () => { + await migrate(); + + await clientDB.delete(users); + + // 创建测试数据 + await clientDB.transaction(async (tx) => { + await tx.insert(users).values({ id: userId }); + }); +}); + +describe('ImporterService', () => { + describe('import sessionGroups', () => { + it('should import session groups and return correct result', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessionGroups: [ + { id: 'group1', name: 'Group 1', createdAt: 1715186011586, updatedAt: 1715186015053 }, + { id: 'group2', name: 'Group 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessionGroups!.added).toBe(2); + expect(result!.sessionGroups!.skips).toBe(0); + expect(result!.sessionGroups!.errors).toBe(0); + + const groups = await clientDB.query.sessionGroups.findMany({ + where: eq(sessionGroups.userId, userId), + }); + expect(groups).toHaveLength(2); + }); + + it('should skip existing session groups and return correct result', async () => { + await clientDB + .insert(sessionGroups) + .values({ clientId: 'group1', name: 'Existing Group', userId }) + .execute(); + + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessionGroups: [ + { id: 'group1', name: 'Group 1', createdAt: 1715186011586, updatedAt: 1715186015053 }, + { id: 'group2', name: 'Group 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessionGroups!.added).toBe(1); + expect(result!.sessionGroups!.skips).toBe(1); + expect(result!.sessionGroups!.errors).toBe(0); + }); + }); + + describe('import sessions', () => { + it('should import sessions and return correct result', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessions!.added).toBe(2); + expect(result!.sessions!.skips).toBe(0); + expect(result!.sessions!.errors).toBe(0); + + const importedSessions = await clientDB.query.sessions.findMany({ + where: eq(sessions.userId, userId), + }); + expect(importedSessions).toHaveLength(2); + + const agentCount = await clientDB.query.agents.findMany({ + where: eq(agents.userId, userId), + }); + + expect(agentCount.length).toBe(2); + + const agentSessionCount = await clientDB.query.agentsToSessions.findMany(); + expect(agentSessionCount.length).toBe(2); + }); + + it('should skip existing sessions and return correct result', async () => { + await clientDB.insert(sessions).values({ clientId: 'session1', userId }).execute(); + + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessions!.added).toBe(1); + expect(result!.sessions!.skips).toBe(1); + expect(result!.sessions!.errors).toBe(0); + }); + + it('should associate imported sessions with session groups', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessionGroups: [ + { id: 'group1', name: 'Group 1', createdAt: 1715186011586, updatedAt: 1715186015053 }, + { id: 'group2', name: 'Group 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + group: 'group1', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + group: 'group2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + { + id: 'session3', + group: 'group4', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 3', + }, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessionGroups!.added).toBe(2); + expect(result!.sessionGroups!.skips).toBe(0); + + expect(result!.sessions!.added).toBe(3); + expect(result!.sessions!.skips).toBe(0); + + // session 1 should be associated with group 1 + const session1 = await clientDB.query.sessions.findFirst({ + where: eq(sessions.clientId, 'session1'), + with: { group: true }, + }); + expect(session1?.group).toBeDefined(); + + // session 3 should not have group + const session3 = await clientDB.query.sessions.findFirst({ + where: eq(sessions.clientId, 'session3'), + with: { group: true }, + }); + expect(session3?.group).toBeNull(); + }); + + it('should create agents and associate them with imported sessions', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'Test Agent 1', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'def', + chatConfig: {} as any, + params: {}, + systemRole: 'Test Agent 2', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + ], + }; + + await service.importData(data); + + // 验证是否为每个 session 创建了对应的 agent + const agentCount = await clientDB.query.agents.findMany({ + where: eq(agents.userId, userId), + }); + expect(agentCount).toHaveLength(2); + + // 验证 agent 的属性是否正确设置 + const agent1 = await clientDB.query.agents.findFirst({ + where: eq(agents.systemRole, 'Test Agent 1'), + }); + expect(agent1?.model).toBe('abc'); + + const agent2 = await clientDB.query.agents.findFirst({ + where: eq(agents.systemRole, 'Test Agent 2'), + }); + expect(agent2?.model).toBe('def'); + + // 验证 agentsToSessions 关联是否正确建立 + const session1 = await clientDB.query.sessions.findFirst({ + where: eq(sessions.clientId, 'session1'), + }); + const session1Agent = await clientDB.query.agentsToSessions.findFirst({ + where: eq(agentsToSessions.sessionId, session1?.id!), + with: { agent: true }, + }); + + expect((session1Agent?.agent as any).systemRole).toBe('Test Agent 1'); + + const session2 = await clientDB.query.sessions.findFirst({ + where: eq(sessions.clientId, 'session2'), + }); + const session2Agent = await clientDB.query.agentsToSessions.findFirst({ + where: eq(agentsToSessions.sessionId, session2?.id!), + with: { agent: true }, + }); + + expect((session2Agent?.agent as any).systemRole).toBe('Test Agent 2'); + }); + + it('should not create duplicate agents for existing sessions', async () => { + // 先导入一些 sessions + await service.importData({ + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'Test Agent 1', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + version: CURRENT_CONFIG_VERSION, + }); + + // 再次导入相同的 sessions + await service.importData({ + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'Test Agent 1', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + version: CURRENT_CONFIG_VERSION, + }); + + // 验证只创建了一个 agent + const agentCount = await clientDB.query.agents.findMany({ + where: eq(agents.userId, userId), + }); + expect(agentCount).toHaveLength(1); + }); + }); + + describe('import topics', () => { + it('should import topics and return correct result', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + topics: [ + { + id: 'topic1', + title: 'Topic 1', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + }, + { + id: 'topic2', + title: 'Topic 2', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session2', + }, + ], + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.topics!.added).toBe(2); + expect(result!.topics!.skips).toBe(0); + expect(result!.topics!.errors).toBe(0); + + const importedTopics = await clientDB.query.topics.findMany({ + where: eq(topics.userId, userId), + }); + expect(importedTopics).toHaveLength(2); + }); + + it('should skip existing topics and return correct result', async () => { + await clientDB + .insert(topics) + .values({ clientId: 'topic1', title: 'Existing Topic', userId }) + .execute(); + + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + topics: [ + { id: 'topic1', title: 'Topic 1', createdAt: 1715186011586, updatedAt: 1715186015053 }, + { id: 'topic2', title: 'Topic 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.topics!.added).toBe(1); + expect(result!.topics!.skips).toBe(1); + expect(result!.topics!.errors).toBe(0); + }); + + it('should associate imported topics with sessions', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + topics: [ + { + id: 'topic1', + title: 'Topic 1', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + }, + { id: 'topic2', title: 'Topic 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + }; + + await service.importData(data); + + // topic1 should be associated with session1 + const [topic1] = await clientDB + .select({ sessionClientId: sessions.clientId }) + .from(topics) + .where(eq(topics.clientId, 'topic1')) + .leftJoin(sessions, eq(topics.sessionId, sessions.id)); + + expect(topic1?.sessionClientId).toBe('session1'); + + // topic2 should not have session + const topic2 = await clientDB.query.topics.findFirst({ + where: eq(topics.clientId, 'topic2'), + with: { session: true }, + }); + expect(topic2?.session).toBeNull(); + }); + }); + + describe('import messages', () => { + it('should import messages and return correct result', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + messages: [ + { + id: 'msg1', + content: 'Message 1', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + topicId: 'topic1', + }, + { + id: 'msg2', + content: 'Message 2', + role: 'assistant', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + topicId: 'topic1', + parentId: 'msg1', + }, + ], + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + topics: [ + { + id: 'topic1', + title: 'Topic 1', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.messages!.added).toBe(2); + expect(result!.messages!.skips).toBe(0); + expect(result!.messages!.errors).toBe(0); + + const importedMessages = await clientDB.query.messages.findMany({ + where: eq(messages.userId, userId), + }); + expect(importedMessages).toHaveLength(2); + }); + + it('should skip existing messages and return correct result', async () => { + await clientDB + .insert(messages) + .values({ + clientId: 'msg1', + content: 'Existing Message', + role: 'user', + userId, + }) + .execute(); + + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + messages: [ + { + id: 'msg1', + content: 'Message 1', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + }, + { + id: 'msg2', + content: 'Message 2', + role: 'assistant', + createdAt: 1715186011586, + updatedAt: 1715186015053, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.messages!.added).toBe(1); + expect(result!.messages!.skips).toBe(1); + expect(result!.messages!.errors).toBe(0); + }); + + it('should associate imported messages with sessions and topics', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + topics: [ + { + id: 'topic1', + title: 'Topic 1', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + }, + ], + messages: [ + { + id: 'msg1', + content: 'Message 1', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + topicId: 'topic1', + }, + { + id: 'msg2', + content: 'Message 2', + role: 'assistant', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + topicId: 'topic1', + parentId: 'msg1', + }, + { + id: 'msg3', + content: 'Message 3', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + }, + ], + }; + + await service.importData(data); + + // msg1 and msg2 should be associated with session1 and topic1 + const [msg1, msg2] = await clientDB.query.messages.findMany({ + where: inArray(messages.clientId, ['msg1', 'msg2']), + with: { + session: true, + topic: true, + }, + }); + + expect(msg1.session?.clientId).toBe('session1'); + expect(msg1.topic?.clientId).toBe('topic1'); + expect(msg2.session?.clientId).toBe('session1'); + expect(msg2.topic?.clientId).toBe('topic1'); + + // msg3 should not have session and topic + const msg3 = await clientDB.query.messages.findFirst({ + where: eq(messages.clientId, 'msg3'), + with: { + session: true, + topic: true, + }, + }); + expect(msg3?.session).toBeNull(); + expect(msg3?.topic).toBeNull(); + }); + + it('should set parentId for messages', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + messages: [ + { + id: 'msg1', + content: 'Message 1', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + }, + { + id: 'msg2', + content: 'Message 2', + role: 'assistant', + createdAt: 1715186011586, + updatedAt: 1715186015053, + parentId: 'msg1', + }, + ], + }; + + await service.importData(data); + + const msg2 = await clientDB.query.messages.findFirst({ + where: eq(messages.clientId, 'msg2'), + with: { parent: true }, + }); + + expect(msg2?.parent?.clientId).toBe('msg1'); + }); + + it('should import parentId Success', () => {}); + }); + + describe('real world examples', () => { + it('should import successfully', async () => { + let result: ImportResults; + + await service.importData( + { + messages: [ + { + role: 'user', + content: 'hello', + files: [], + sessionId: 'inbox', + topicId: '2wcF8yaS', + createdAt: 1714236590340, + id: 'DCG1G1EH', + updatedAt: 1714236590340, + extra: {}, + }, + { + role: 'assistant', + content: '...', + parentId: 'DCG1G1EH', + sessionId: 'inbox', + topicId: '2wcF8yaS', + createdAt: 1714236590441, + id: 'gY41w5vQ', + updatedAt: 1714236590518, + error: { + body: { + error: { + message: "model 'mixtral' not found, try pulling it first", + name: 'ResponseError', + status_code: 404, + }, + provider: 'ollama', + }, + message: + 'Error requesting Ollama service, please troubleshoot or retry based on the following information', + type: 'OllamaBizError', + }, + extra: { fromModel: 'mixtral', fromProvider: 'ollama' }, + }, + { + role: 'user', + content: 'hello', + files: [], + sessionId: 'a5fefc88-f6c1-44fb-9e98-3d366b1ed589', + topicId: 'v38snJ0A', + createdAt: 1717080410895, + id: 'qOIxEGEB', + updatedAt: 1717080410895, + extra: {}, + }, + { + role: 'assistant', + content: '...', + parentId: 'qOIxEGEB', + sessionId: 'a5fefc88-f6c1-44fb-9e98-3d366b1ed589', + topicId: 'v38snJ0A', + createdAt: 1717080410970, + id: 'w28FcqY5', + updatedAt: 1717080411485, + error: { + body: { error: { errorType: 'NoOpenAIAPIKey' }, provider: 'openai' }, + message: 'OpenAI API Key is empty, please add a custom OpenAI API Key', + type: 'NoOpenAIAPIKey', + }, + extra: { fromModel: 'gpt-3.5-turbo', fromProvider: 'openai' }, + }, + ], + sessionGroups: [ + { + name: 'Writter', + sort: 0, + createdAt: 1706114744425, + id: 'XlUbvOvL', + updatedAt: 1706114747468, + }, + ], + sessions: [ + { + config: { + model: 'gpt-3.5-turbo', + params: { + frequency_penalty: 0, + presence_penalty: 0, + temperature: 0.6, + top_p: 1, + }, + plugins: [], + systemRole: + "You are a LobeChat technical operator 🍐🐊. You now need to write a developer's guide for LobeChat as a guide for them to develop LobeChat. This guide will include several sections, and you need to output the corresponding document content based on the user's input.\n\nHere is the technical introduction of LobeChat\n\n LobeChat is an AI conversation application built with the Next.js framework. It uses a series of technology stacks to implement various functions and features.\n\n\n ## Basic Technology Stack\n\n The core technology stack of LobeChat is as follows:\n\n - **Framework**: We chose [Next.js](https://nextjs.org/), a powerful React framework that provides key features such as server-side rendering, routing framework, and Router Handler for our project.\n - **Component Library**: We use [Ant Design (antd)](https://ant.design/) as the basic component library, and introduce [lobe-ui](https://github.com/lobehub/lobe-ui) as our business component library.\n - **State Management**: We use [zustand](https://github.com/pmndrs/zustand), a lightweight and easy-to-use state management library.\n - **Network Request**: We adopt [swr](https://swr.vercel.app/), a React Hooks library for data fetching.\n - **Routing**: We directly use the routing solution provided by [Next.js](https://nextjs.org/) itself.\n - **Internationalization**: We use [i18next](https://www.i18next.com/) to implement multi-language support for the application.\n - **Styling**: We use [antd-style](https://github.com/ant-design/antd-style), a CSS-in-JS library that is compatible with Ant Design.\n - **Unit Testing**: We use [vitest](https://github.com/vitejs/vitest) for unit testing.\n\n ## Folder Directory Structure\n\n The folder directory structure of LobeChat is as follows:\n\n \\`\\`\\`bash\n src\n ├── app # Main logic and state management related code of the application\n ├── components # Reusable UI components\n ├── config # Application configuration files, including client environment variables and server environment variables\n ├── const # Used to define constants, such as action types, route names, etc.\n ├── features # Function modules related to business functions, such as Agent settings, plugin development pop-ups, etc.\n ├── hooks # Custom utility Hooks reused throughout the application\n ├── layout # Layout components of the application, such as navigation bar, sidebar, etc.\n ├── locales # Language files for internationalization\n ├── services # Encapsulated backend service interfaces, such as HTTP requests\n ├── store # Zustand store for state management\n ├── types # TypeScript type definition files\n └── utils # Common utility functions\n \\`\\`\\`\n", + tts: { + showAllLocaleVoice: false, + sttLocale: 'auto', + ttsService: 'openai', + voice: { openai: 'alloy' }, + }, + chatConfig: { + autoCreateTopicThreshold: 2, + displayMode: 'chat', + enableAutoCreateTopic: true, + historyCount: 1, + }, + }, + group: 'XlUbvOvL', + meta: { + avatar: '📝', + description: + 'LobeChat is an AI conversation application built with the Next.js framework. I will help you write the development documentation for LobeChat.', + tags: [ + 'Development Documentation', + 'Technical Introduction', + 'next-js', + 'react', + 'lobe-chat', + ], + title: 'LobeChat Technical Documentation Expert', + }, + type: 'agent', + createdAt: '2024-01-24T16:43:12.164Z', + id: 'a5fefc88-f6c1-44fb-9e98-3d366b1ed589', + updatedAt: '2024-01-24T16:46:15.226Z', + pinned: false, + }, + ], + topics: [ + { + title: 'Default Topic', + sessionId: 'inbox', + createdAt: 1714236590531, + id: '2wcF8yaS', + updatedAt: 1714236590531, + }, + { + title: 'Default Topic', + sessionId: 'a5fefc88-f6c1-44fb-9e98-3d366b1ed589', + createdAt: 1717080410825, + id: 'v38snJ0A', + updatedAt: 1717080410825, + }, + ], + version: mockImportData.version, + }, + { onSuccess: (res) => (result = res) }, + ); + + expect(result!).toEqual({ + sessionGroups: { added: 1, errors: 0, skips: 0 }, + sessions: { added: 1, errors: 0, skips: 0 }, + topics: { added: 2, errors: 0, skips: 0 }, + messages: { added: 4, errors: 0, skips: 0 }, + }); + }); + + it('should import real world data', async () => { + let result: ImportResults; + + await service.importData( + { ...(mockImportData.state as any), version: mockImportData.version }, + { + onSuccess: (res) => { + result = res; + }, + }, + ); + + expect(result!).toEqual({ + sessionGroups: { added: 2, errors: 0, skips: 0 }, + sessions: { added: 15, errors: 0, skips: 0 }, + topics: { added: 4, errors: 0, skips: 0 }, + messages: { added: 32, errors: 0, skips: 0 }, + }); + }); + }); +}); diff --git a/src/services/import/client.ts b/src/services/import/client.ts index a148abd28d57..d18a09507248 100644 --- a/src/services/import/client.ts +++ b/src/services/import/client.ts @@ -1,74 +1,34 @@ -import { MessageModel } from '@/database/_deprecated/models/message'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { SessionGroupModel } from '@/database/_deprecated/models/sessionGroup'; -import { TopicModel } from '@/database/_deprecated/models/topic'; -import { ImportResult, ImportResults } from '@/services/config'; +import { clientDB } from '@/database/client/db'; +import { DataImporterRepos } from '@/database/repositories/dataImporter'; import { useUserStore } from '@/store/user'; import { ImportStage, ImporterEntryData, OnImportCallbacks } from '@/types/importer'; import { UserSettings } from '@/types/user/settings'; export class ClientService { + private dataImporter: DataImporterRepos; + constructor(userId: string) { + this.dataImporter = new DataImporterRepos(clientDB as any, userId); + } + importSettings = async (settings: UserSettings) => { await useUserStore.getState().importAppSettings(settings); }; - importData = async ( - config: ImporterEntryData, - callbacks?: OnImportCallbacks, - ): Promise => { + importData = async (data: ImporterEntryData, callbacks?: OnImportCallbacks) => { callbacks?.onStageChange?.(ImportStage.Importing); const time = Date.now(); - - const { messages = [], sessionGroups = [], sessions = [], topics = [] } = config; - - let messageResult: ImportResult | undefined; - let sessionResult: ImportResult | undefined; - let sessionGroupResult: ImportResult | undefined; - let topicResult: ImportResult | undefined; - - if (messages.length > 0) { - const res = await MessageModel.batchCreate(messages as any); - messageResult = this.mapImportResult(res); - } - - if (sessionGroups.length > 0) { - const res = await SessionGroupModel.batchCreate(sessionGroups as any); - sessionGroupResult = this.mapImportResult(res); - } - - if (topics.length > 0) { - const res = await TopicModel.batchCreate(topics as any); - topicResult = this.mapImportResult(res); + try { + const result = await this.dataImporter.importData(data); + const duration = Date.now() - time; + + callbacks?.onStageChange?.(ImportStage.Success); + callbacks?.onSuccess?.(result, duration); + } catch (e) { + console.error(e); + callbacks?.onStageChange?.(ImportStage.Error); + const error = e as Error; + + callbacks?.onError?.({ code: 'ImportError', httpStatus: 0, message: error.message }); } - - if (sessions.length > 0) { - const data = await SessionModel.batchCreate(sessions as any); - sessionResult = this.mapImportResult(data); - } - - const result = { - messages: messageResult, - sessionGroups: sessionGroupResult, - sessions: sessionResult, - topics: topicResult, - }; - - const duration = Date.now() - time; - callbacks?.onStageChange?.(ImportStage.Success); - callbacks?.onSuccess?.(result, duration); - - return result; - }; - - private mapImportResult = (input: { - added: number; - errors?: Error[]; - skips: string[]; - }): ImportResult => { - return { - added: input.added, - errors: input.errors?.length || 0, - skips: input.skips.length, - }; }; } diff --git a/src/services/import/index.ts b/src/services/import/index.ts index 4f2ab75bfea8..a94e4e6d1de6 100644 --- a/src/services/import/index.ts +++ b/src/services/import/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const importService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/services/message/client.test.ts b/src/services/message/client.test.ts index 867fda6c5b72..eced3f893d2c 100644 --- a/src/services/message/client.test.ts +++ b/src/services/message/client.test.ts @@ -1,133 +1,156 @@ import dayjs from 'dayjs'; -import { Mock, describe, expect, it, vi } from 'vitest'; +import { and, eq } from 'drizzle-orm'; +import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CreateMessageParams, MessageModel } from '@/database/_deprecated/models/message'; +import { MessageModel } from '@/database/_deprecated/models/message'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { + MessageItem, + files, + messagePlugins, + messageTTS, + messageTranslates, + messages, + sessions, + topics, + users, +} from '@/database/schemas'; import { ChatMessage, ChatMessageError, - ChatPluginPayload, ChatTTS, ChatTranslate, + CreateMessageParams, } from '@/types/message'; import { ClientService } from './client'; -const messageService = new ClientService(); - -// Mock the MessageModel -vi.mock('@/database/_deprecated/models/message', () => { - return { - MessageModel: { - create: vi.fn(), - batchCreate: vi.fn(), - count: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - bulkDelete: vi.fn(), - queryBySessionId: vi.fn(), - update: vi.fn(), - updatePlugin: vi.fn(), - batchDelete: vi.fn(), - clearTable: vi.fn(), - batchUpdate: vi.fn(), - queryAll: vi.fn(), - updatePluginState: vi.fn(), - }, - }; +const userId = 'message-db'; +const sessionId = '1'; +const topicId = 'topic-id'; + +// Mock data +const mockMessageId = 'mock-message-id'; +const mockMessage = { + id: mockMessageId, + content: 'Mock message content', + sessionId, + role: 'user', +} as ChatMessage; + +const mockMessages = [mockMessage]; + +beforeEach(async () => { + await migrate(); + + // 在每个测试用例之前,清空表 + await clientDB.transaction(async (trx) => { + await trx.delete(users); + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + + await trx.insert(sessions).values([{ id: sessionId, userId }]); + await trx.insert(topics).values([{ id: topicId, sessionId, userId }]); + await trx.insert(files).values({ + id: 'f1', + userId: userId, + url: 'abc', + name: 'file-1', + fileType: 'image/png', + size: 1000, + }); + }); }); -describe('MessageClientService', () => { - // Mock data - const mockMessageId = 'mock-message-id'; - const mockMessage = { - id: mockMessageId, - content: 'Mock message content', - sessionId: 'mock-session-id', - createdAt: 100, - updatedAt: 100, - role: 'user', - // ... other properties - } as ChatMessage; - const mockMessages = [mockMessage]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); +afterEach(async () => { + // 在每个测试用例之后,清空表 + await clientDB.delete(users); +}); + +const messageService = new ClientService(userId); +describe('MessageClientService', () => { describe('create', () => { it('should create a message and return its id', async () => { // Setup - const createParams = { + const createParams: CreateMessageParams = { content: 'New message content', - sessionId: '1', - // ... other properties - } as CreateMessageParams; - (MessageModel.create as Mock).mockResolvedValue({ id: mockMessageId }); + sessionId, + role: 'user', + }; // Execute const messageId = await messageService.createMessage(createParams); // Assert - expect(MessageModel.create).toHaveBeenCalledWith(createParams); - expect(messageId).toBe(mockMessageId); + expect(messageId).toMatch(/^msg_/); }); }); describe('batchCreate', () => { it('should batch create messages', async () => { - // Setup - (MessageModel.batchCreate as Mock).mockResolvedValue(mockMessages); - // Execute - const result = await messageService.batchCreateMessages(mockMessages); + await messageService.batchCreateMessages([ + { + content: 'Mock message content', + sessionId, + role: 'user', + }, + { + content: 'Mock message content', + sessionId, + role: 'user', + }, + ] as MessageItem[]); + const count = await clientDB.$count(messages); // Assert - expect(MessageModel.batchCreate).toHaveBeenCalledWith(mockMessages); - expect(result).toBe(mockMessages); + expect(count).toBe(2); }); }); describe('removeMessage', () => { it('should remove a message by id', async () => { - // Setup - (MessageModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await messageService.removeMessage(mockMessageId); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await messageService.removeMessage(mockMessageId); // Assert - expect(MessageModel.delete).toHaveBeenCalledWith(mockMessageId); - expect(result).toBe(true); + const count = await clientDB.$count(messages); + + expect(count).toBe(0); }); }); describe('removeMessages', () => { it('should remove a message by id', async () => { // Setup - (MessageModel.bulkDelete as Mock).mockResolvedValue(true); + await clientDB.insert(messages).values([ + { id: mockMessageId, role: 'user', userId }, + { role: 'assistant', userId }, + ]); // Execute - const result = await messageService.removeMessages([mockMessageId]); + await messageService.removeMessages([mockMessageId]); // Assert - expect(MessageModel.bulkDelete).toHaveBeenCalledWith([mockMessageId]); - expect(result).toBe(true); + const count = await clientDB.$count(messages); + + expect(count).toBe(1); }); }); describe('getMessages', () => { it('should retrieve messages by sessionId and topicId', async () => { // Setup - const sessionId = 'session-id'; - const topicId = 'topic-id'; - (MessageModel.query as Mock).mockResolvedValue(mockMessages); + await clientDB + .insert(messages) + .values({ id: mockMessageId, sessionId, topicId, role: 'user', userId }); // Execute - const messages = await messageService.getMessages(sessionId, topicId); + const data = await messageService.getMessages(sessionId, topicId); // Assert - expect(MessageModel.query).toHaveBeenCalledWith({ sessionId, topicId }); - expect(messages).toEqual(mockMessages.map((i) => ({ ...i, imageList: [] }))); + expect(data[0]).toMatchObject({ id: mockMessageId, role: 'user' }); }); }); @@ -135,14 +158,21 @@ describe('MessageClientService', () => { it('should retrieve all messages in a session', async () => { // Setup const sessionId = 'session-id'; - (MessageModel.queryBySessionId as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(sessions).values([ + { id: 'bbb', userId }, + { id: sessionId, userId }, + ]); + await clientDB.insert(messages).values([ + { sessionId, topicId, role: 'user', userId }, + { sessionId, topicId, role: 'assistant', userId }, + { sessionId: 'bbb', topicId, role: 'assistant', userId }, + ]); // Execute - const messages = await messageService.getAllMessagesInSession(sessionId); + const data = await messageService.getAllMessagesInSession(sessionId); // Assert - expect(MessageModel.queryBySessionId).toHaveBeenCalledWith(sessionId); - expect(messages).toBe(mockMessages); + expect(data.length).toBe(2); }); }); @@ -150,77 +180,85 @@ describe('MessageClientService', () => { it('should batch remove messages by assistantId and topicId', async () => { // Setup const assistantId = 'assistant-id'; - const topicId = 'topic-id'; - (MessageModel.batchDelete as Mock).mockResolvedValue(true); + const sessionId = 'session-id'; + await clientDB.insert(sessions).values([ + { id: 'bbb', userId }, + { id: sessionId, userId }, + ]); + await clientDB.insert(messages).values([ + { sessionId, topicId, role: 'user', userId }, + { sessionId, topicId, role: 'assistant', userId }, + { sessionId: 'bbb', topicId, role: 'assistant', userId }, + ]); // Execute - const result = await messageService.removeMessagesByAssistant(assistantId, topicId); + await messageService.removeMessagesByAssistant(sessionId, topicId); // Assert - expect(MessageModel.batchDelete).toHaveBeenCalledWith(assistantId, topicId); - expect(result).toBe(true); + const result = await clientDB.query.messages.findMany({ + where: and(eq(messages.sessionId, sessionId), eq(messages.topicId, topicId)), + }); + + expect(result.length).toBe(0); }); }); describe('clearAllMessage', () => { it('should clear all messages from the table', async () => { // Setup - (MessageModel.clearTable as Mock).mockResolvedValue(true); + await clientDB.insert(users).values({ id: 'another' }); + await clientDB.insert(messages).values([ + { id: mockMessageId, role: 'user', userId }, + { role: 'user', userId: 'another' }, + ]); // Execute - const result = await messageService.removeAllMessages(); + await messageService.removeAllMessages(); // Assert - expect(MessageModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); - }); - }); - - describe('bindMessagesToTopic', () => { - it('should batch update messages to bind them to a topic', async () => { - // Setup - const topicId = 'topic-id'; - const messageIds = [mockMessageId]; - (MessageModel.batchUpdate as Mock).mockResolvedValue(mockMessages); - - // Execute - const result = await messageService.bindMessagesToTopic(topicId, messageIds); - - // Assert - expect(MessageModel.batchUpdate).toHaveBeenCalledWith(messageIds, { topicId }); - expect(result).toBe(mockMessages); + const result = await clientDB.query.messages.findMany({ + where: eq(messages.userId, userId), + }); + expect(result.length).toBe(0); }); }); describe('getAllMessages', () => { it('should retrieve all messages', async () => { - // Setup - (MessageModel.queryAll as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(messages).values([ + { sessionId, topicId, content: '1', role: 'user', userId }, + { sessionId, topicId, content: '2', role: 'assistant', userId }, + ]); // Execute - const messages = await messageService.getAllMessages(); + const data = await messageService.getAllMessages(); // Assert - expect(MessageModel.queryAll).toHaveBeenCalled(); - expect(messages).toBe(mockMessages); + expect(data).toMatchObject([ + { sessionId, topicId, content: '1', role: 'user', userId }, + { sessionId, topicId, content: '2', role: 'assistant', userId }, + ]); }); }); describe('updateMessageError', () => { it('should update the error field of a message', async () => { // Setup + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); const newError = { type: 'InvalidProviderAPIKey', message: 'Error occurred', } as ChatMessageError; - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, error: newError }); // Execute - const result = await messageService.updateMessageError(mockMessageId, newError); + await messageService.updateMessageError(mockMessageId, newError); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { error: newError }); - expect(result).toEqual({ ...mockMessage, error: newError }); + const result = await clientDB.query.messages.findFirst({ + where: eq(messages.id, mockMessageId), + }); + + expect(result!.error).toEqual(newError); }); }); @@ -248,88 +286,85 @@ describe('MessageClientService', () => { describe('updateMessagePluginState', () => { it('should update the plugin state of a message', async () => { // Setup + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const key = 'stateKey'; const value = 'stateValue'; const newPluginState = { [key]: value }; - (MessageModel.updatePluginState as Mock).mockResolvedValue({ - ...mockMessage, - pluginState: newPluginState, - }); // Execute - const result = await messageService.updateMessagePluginState(mockMessageId, { key: value }); + await messageService.updateMessagePluginState(mockMessageId, { stateKey: value }); // Assert - expect(MessageModel.updatePluginState).toHaveBeenCalledWith(mockMessageId, { key: value }); - expect(result).toEqual({ ...mockMessage, pluginState: newPluginState }); + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messagePlugins.id, mockMessageId), + }); + expect(result!.state).toEqual(newPluginState); }); }); describe('updateMessagePluginArguments', () => { it('should update the plugin arguments object of a message', async () => { // Setup - const key = 'stateKey'; + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const value = 'stateValue'; - (MessageModel.updatePlugin as Mock).mockResolvedValue({}); // Execute await messageService.updateMessagePluginArguments(mockMessageId, { key: value }); // Assert - expect(MessageModel.updatePlugin).toHaveBeenCalledWith(mockMessageId, { - arguments: '{"key":"stateValue"}', + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messageTTS.id, mockMessageId), }); + expect(result).toMatchObject({ arguments: '{"key":"stateValue"}' }); }); it('should update the plugin arguments string of a message', async () => { // Setup - const key = 'stateKey'; + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const value = 'stateValue'; - (MessageModel.updatePlugin as Mock).mockResolvedValue({}); - // Execute await messageService.updateMessagePluginArguments( mockMessageId, - JSON.stringify({ key: value }), + JSON.stringify({ abc: value }), ); // Assert - expect(MessageModel.updatePlugin).toHaveBeenCalledWith(mockMessageId, { - arguments: '{"key":"stateValue"}', + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messageTTS.id, mockMessageId), }); + expect(result).toMatchObject({ arguments: '{"abc":"stateValue"}' }); }); }); describe('countMessages', () => { it('should count the total number of messages', async () => { // Setup - const mockCount = 10; - (MessageModel.count as Mock).mockResolvedValue(mockCount); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); // Execute const count = await messageService.countMessages(); // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(count).toBe(mockCount); + expect(count).toBe(1); }); }); describe('countTodayMessages', () => { it('should count the number of messages created today', async () => { // Setup - const today = dayjs().format('YYYY-MM-DD'); const mockMessages = [ - { ...mockMessage, createdAt: today }, - { ...mockMessage, createdAt: today }, - { ...mockMessage, createdAt: '2023-01-01' }, + { ...mockMessage, id: undefined, createdAt: new Date(), userId }, + { ...mockMessage, id: undefined, createdAt: new Date(), userId }, + { ...mockMessage, id: undefined, createdAt: new Date('2023-01-01'), userId }, ]; - (MessageModel.queryAll as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(messages).values(mockMessages); // Execute const count = await messageService.countTodayMessages(); // Assert - expect(MessageModel.queryAll).toHaveBeenCalled(); expect(count).toBe(2); }); }); @@ -337,45 +372,46 @@ describe('MessageClientService', () => { describe('updateMessageTTS', () => { it('should update the TTS field of a message', async () => { // Setup - const newTTS: ChatTTS = { - contentMd5: 'abc', - file: 'file-abc', - }; - - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, tts: newTTS }); + await clientDB + .insert(files) + .values({ id: 'file-abc', fileType: 'text', name: 'abc', url: 'abc', size: 100, userId }); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + const newTTS: ChatTTS = { contentMd5: 'abc', file: 'file-abc' }; // Execute - const result = await messageService.updateMessageTTS(mockMessageId, newTTS); + await messageService.updateMessageTTS(mockMessageId, newTTS); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { tts: newTTS }); - expect(result).toEqual({ ...mockMessage, tts: newTTS }); + const result = await clientDB.query.messageTTS.findFirst({ + where: eq(messageTTS.id, mockMessageId), + }); + + expect(result).toMatchObject({ contentMd5: 'abc', fileId: 'file-abc', id: mockMessageId }); }); }); describe('updateMessageTranslate', () => { it('should update the translate field of a message', async () => { // Setup - const newTranslate: ChatTranslate = { - content: 'Translated text', - to: 'es', - }; - - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, translate: newTranslate }); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + const newTranslate: ChatTranslate = { content: 'Translated text', to: 'es' }; // Execute - const result = await messageService.updateMessageTranslate(mockMessageId, newTranslate); + await messageService.updateMessageTranslate(mockMessageId, newTranslate); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { translate: newTranslate }); - expect(result).toEqual({ ...mockMessage, translate: newTranslate }); + const result = await clientDB.query.messageTranslates.findFirst({ + where: eq(messageTranslates.id, mockMessageId), + }); + + expect(result).toMatchObject(newTranslate); }); }); describe('hasMessages', () => { it('should return true if there are messages', async () => { // Setup - (MessageModel.count as Mock).mockResolvedValue(1); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); // Execute const result = await messageService.hasMessages(); @@ -385,9 +421,6 @@ describe('MessageClientService', () => { }); it('should return false if there are no messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(0); - // Execute const result = await messageService.hasMessages(); diff --git a/src/services/message/client.ts b/src/services/message/client.ts index d51d6d72a1a4..56d3e59fd57b 100644 --- a/src/services/message/client.ts +++ b/src/services/message/client.ts @@ -1,10 +1,9 @@ import dayjs from 'dayjs'; -import { FileModel } from '@/database/_deprecated/models/file'; -import { MessageModel } from '@/database/_deprecated/models/message'; -import { DB_Message } from '@/database/_deprecated/schemas/message'; +import { clientDB } from '@/database/client/db'; +import { MessageItem } from '@/database/schemas'; +import { MessageModel } from '@/database/server/models/message'; import { - ChatFileItem, ChatMessage, ChatMessageError, ChatTTS, @@ -15,101 +14,85 @@ import { import { IMessageService } from './type'; export class ClientService implements IMessageService { + private messageModel: MessageModel; + + constructor(userId: string) { + this.messageModel = new MessageModel(clientDB as any, userId); + } + async createMessage(data: CreateMessageParams) { - const { id } = await MessageModel.create(data); + const { id } = await this.messageModel.create(data); return id; } - async batchCreateMessages(messages: ChatMessage[]) { - return MessageModel.batchCreate(messages); + async batchCreateMessages(messages: MessageItem[]) { + return this.messageModel.batchCreate(messages); } async getMessages(sessionId: string, topicId?: string): Promise { - const messages = await MessageModel.query({ sessionId, topicId }); - - const fileList = (await Promise.all( - messages - .flatMap((item) => item.files) - .filter(Boolean) - .map(async (id) => FileModel.findById(id!)), - )) as ChatFileItem[]; - - return messages.map((item) => ({ - ...item, - imageList: fileList - .filter((file) => item.files?.includes(file.id) && file.fileType.startsWith('image')) - .map((file) => ({ - alt: file.name, - id: file.id, - url: file.url, - })), - })); + return this.messageModel.query({ sessionId, topicId }); } async getAllMessages() { - return MessageModel.queryAll(); + return this.messageModel.queryAll(); } async countMessages() { - return MessageModel.count(); + return this.messageModel.count(); } async countTodayMessages() { - const topics = await MessageModel.queryAll(); + const topics = await this.messageModel.queryAll(); return topics.filter( (item) => dayjs(item.createdAt).format('YYYY-MM-DD') === dayjs().format('YYYY-MM-DD'), ).length; } async getAllMessagesInSession(sessionId: string) { - return MessageModel.queryBySessionId(sessionId); + return this.messageModel.queryBySessionId(sessionId); } async updateMessageError(id: string, error: ChatMessageError) { - return MessageModel.update(id, { error }); + return this.messageModel.update(id, { error }); } - async updateMessage(id: string, message: Partial) { - return MessageModel.update(id, message); + async updateMessage(id: string, message: Partial) { + return this.messageModel.update(id, message); } async updateMessageTTS(id: string, tts: Partial | false) { - return MessageModel.update(id, { tts }); + return this.messageModel.updateTTS(id, tts as any); } async updateMessageTranslate(id: string, translate: Partial | false) { - return MessageModel.update(id, { translate }); + return this.messageModel.updateTranslate(id, translate as any); } async updateMessagePluginState(id: string, value: Record) { - return MessageModel.updatePluginState(id, value); + return this.messageModel.updatePluginState(id, value); } async updateMessagePluginArguments(id: string, value: string | Record) { const args = typeof value === 'string' ? value : JSON.stringify(value); - return MessageModel.updatePlugin(id, { arguments: args }); - } - - async bindMessagesToTopic(topicId: string, messageIds: string[]) { - return MessageModel.batchUpdate(messageIds, { topicId }); + return this.messageModel.updateMessagePlugin(id, { arguments: args }); } async removeMessage(id: string) { - return MessageModel.delete(id); + return this.messageModel.deleteMessage(id); } async removeMessages(ids: string[]) { - return MessageModel.bulkDelete(ids); + return this.messageModel.deleteMessages(ids); } async removeMessagesByAssistant(assistantId: string, topicId?: string) { - return MessageModel.batchDelete(assistantId, topicId); + return this.messageModel.deleteMessagesBySession(assistantId, topicId); } async removeAllMessages() { - return MessageModel.clearTable(); + return this.messageModel.deleteAllMessages(); } async hasMessages() { diff --git a/src/services/message/index.test.ts b/src/services/message/index.test.ts deleted file mode 100644 index 625261f53c90..000000000000 --- a/src/services/message/index.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Mock, describe, expect, it, vi } from 'vitest'; - -import { CreateMessageParams, MessageModel } from '@/database/_deprecated/models/message'; -import { ChatMessage, ChatMessageError, ChatPluginPayload } from '@/types/message'; - -import { messageService } from './index'; - -// Mock the MessageModel -vi.mock('@/database/_deprecated/models/message', () => { - return { - MessageModel: { - count: vi.fn(), - }, - }; -}); - -describe('MessageService', () => { - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); - - describe('hasMessages', () => { - it('should return true if there are messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(1); - - // Execute - const hasMessages = await messageService.hasMessages(); - - // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(hasMessages).toBe(true); - }); - - it('should return false if there are no messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(0); - - // Execute - const hasMessages = await messageService.hasMessages(); - - // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(hasMessages).toBe(false); - }); - }); -}); diff --git a/src/services/message/index.ts b/src/services/message/index.ts index 930eaf6034ed..c1c1fbdb542b 100644 --- a/src/services/message/index.ts +++ b/src/services/message/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const messageService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/services/message/server.ts b/src/services/message/server.ts index 6562b4ec3e25..6860ede39df9 100644 --- a/src/services/message/server.ts +++ b/src/services/message/server.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { INBOX_SESSION_ID } from '@/const/session'; +import { MessageItem } from '@/database/schemas'; import { lambdaClient } from '@/libs/trpc/client'; import { ChatMessage, @@ -19,7 +20,7 @@ export class ServerService implements IMessageService { }); } - batchCreateMessages(messages: ChatMessage[]): Promise { + batchCreateMessages(messages: MessageItem[]): Promise { return lambdaClient.message.batchCreateMessages.mutate(messages); } @@ -33,6 +34,7 @@ export class ServerService implements IMessageService { getAllMessages(): Promise { return lambdaClient.message.getAllMessages.query(); } + getAllMessagesInSession(sessionId: string): Promise { return lambdaClient.message.getAllMessagesInSession.query({ sessionId: this.toDbSessionId(sessionId), @@ -79,10 +81,6 @@ export class ServerService implements IMessageService { return lambdaClient.message.updatePluginState.mutate({ id, value }); } - bindMessagesToTopic(_topicId: string, _messageIds: string[]): Promise { - throw new Error('Method not implemented.'); - } - removeMessage(id: string): Promise { return lambdaClient.message.removeMessage.mutate({ id }); } diff --git a/src/services/message/type.ts b/src/services/message/type.ts index 2929620ad128..4e197ddd0c6e 100644 --- a/src/services/message/type.ts +++ b/src/services/message/type.ts @@ -1,4 +1,5 @@ import { DB_Message } from '@/database/_deprecated/schemas/message'; +import { MessageItem } from '@/database/schemas'; import { ChatMessage, ChatMessageError, @@ -11,7 +12,7 @@ import { export interface IMessageService { createMessage(data: CreateMessageParams): Promise; - batchCreateMessages(messages: ChatMessage[]): Promise; + batchCreateMessages(messages: MessageItem[]): Promise; getMessages(sessionId: string, topicId?: string): Promise; getAllMessages(): Promise; @@ -20,11 +21,10 @@ export interface IMessageService { countTodayMessages(): Promise; updateMessageError(id: string, error: ChatMessageError): Promise; - updateMessage(id: string, message: Partial): Promise; + updateMessage(id: string, message: Partial): Promise; updateMessageTTS(id: string, tts: Partial | false): Promise; updateMessageTranslate(id: string, translate: Partial | false): Promise; updateMessagePluginState(id: string, value: Record): Promise; - bindMessagesToTopic(topicId: string, messageIds: string[]): Promise; removeMessage(id: string): Promise; removeMessages(ids: string[]): Promise; diff --git a/src/services/plugin/client.test.ts b/src/services/plugin/client.test.ts index e2b6ccc66822..b2a0f2195fb5 100644 --- a/src/services/plugin/client.test.ts +++ b/src/services/plugin/client.test.ts @@ -1,30 +1,30 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { PluginModel } from '@/database/_deprecated/models/plugin'; -import { DB_Plugin } from '@/database/_deprecated/schemas/plugin'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { installedPlugins, users } from '@/database/schemas'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { ClientService } from './client'; import { InstallPluginParams } from './type'; -const pluginService = new ClientService(); - // Mocking modules and functions -vi.mock('@/database/_deprecated/models/plugin', () => ({ - PluginModel: { - getList: vi.fn(), - create: vi.fn(), - delete: vi.fn(), - update: vi.fn(), - clear: vi.fn(), - }, -})); - -beforeEach(() => { - vi.resetAllMocks(); +const userId = 'message-db'; +const pluginService = new ClientService(userId); + +// Mock data +beforeEach(async () => { + await migrate(); + + // 在每个测试用例之前,重置表数据 + await clientDB.transaction(async (trx) => { + await trx.delete(users); + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + }); }); describe('PluginService', () => { @@ -32,18 +32,19 @@ describe('PluginService', () => { it('should install a plugin', async () => { // Arrange const fakePlugin = { - identifier: 'test-plugin', + identifier: 'test-plugin-d', manifest: { name: 'TestPlugin', version: '1.0.0' } as unknown as LobeChatPluginManifest, type: 'plugin', } as InstallPluginParams; - vi.mocked(PluginModel.create).mockResolvedValue(fakePlugin); // Act - const installedPlugin = await pluginService.installPlugin(fakePlugin); + await pluginService.installPlugin(fakePlugin); // Assert - expect(PluginModel.create).toHaveBeenCalledWith(fakePlugin); - expect(installedPlugin).toEqual(fakePlugin); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, fakePlugin.identifier), + }); + expect(result).toMatchObject(fakePlugin); }); }); @@ -51,14 +52,14 @@ describe('PluginService', () => { it('should return a list of installed plugins', async () => { // Arrange const fakePlugins = [{ identifier: 'test-plugin', type: 'plugin' }] as LobeTool[]; - vi.mocked(PluginModel.getList).mockResolvedValue(fakePlugins as DB_Plugin[]); - + await clientDB + .insert(installedPlugins) + .values([{ identifier: 'test-plugin', type: 'plugin', userId }]); // Act - const installedPlugins = await pluginService.getInstalledPlugins(); + const data = await pluginService.getInstalledPlugins(); // Assert - expect(PluginModel.getList).toHaveBeenCalled(); - expect(installedPlugins).toEqual(fakePlugins); + expect(data).toMatchObject(fakePlugins); }); }); @@ -66,13 +67,15 @@ describe('PluginService', () => { it('should uninstall a plugin', async () => { // Arrange const identifier = 'test-plugin'; - vi.mocked(PluginModel.delete).mockResolvedValue(); + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.uninstallPlugin(identifier); + await pluginService.uninstallPlugin(identifier); // Assert - expect(PluginModel.delete).toHaveBeenCalledWith(identifier); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); expect(result).toBe(undefined); }); }); @@ -81,67 +84,74 @@ describe('PluginService', () => { it('should create a custom plugin', async () => { // Arrange const customPlugin = { - identifier: 'custom-plugin', + identifier: 'custom-plugin-x', manifest: {}, type: 'customPlugin', } as LobeToolCustomPlugin; - vi.mocked(PluginModel.create).mockResolvedValue(customPlugin); // Act - const result = await pluginService.createCustomPlugin(customPlugin); + await pluginService.createCustomPlugin(customPlugin); // Assert - expect(PluginModel.create).toHaveBeenCalledWith({ - ...customPlugin, - type: 'customPlugin', + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, customPlugin.identifier), }); - expect(result).toEqual(customPlugin); + expect(result).toMatchObject(customPlugin); }); }); describe('updatePlugin', () => { it('should update a plugin', async () => { // Arrange - const id = 'plugin-id'; - const value = { settings: { ab: '1' } } as unknown as LobeToolCustomPlugin; - vi.mocked(PluginModel.update).mockResolvedValue(1); + const identifier = 'plugin-id'; + const value = { customParams: { ab: '1' } } as unknown as LobeToolCustomPlugin; + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePlugin(id, value); + await pluginService.updatePlugin(identifier, value); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, value); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); + expect(result).toMatchObject(value); }); }); describe('updatePluginManifest', () => { it('should update a plugin manifest', async () => { // Arrange - const id = 'plugin-id'; + const identifier = 'plugin-id'; const manifest = { name: 'NewPluginManifest' } as unknown as LobeChatPluginManifest; - vi.mocked(PluginModel.update).mockResolvedValue(1); + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePluginManifest(id, manifest); + await pluginService.updatePluginManifest(identifier, manifest); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, { manifest }); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); + expect(result).toMatchObject({ manifest }); }); }); describe('removeAllPlugins', () => { it('should remove all plugins', async () => { // Arrange - vi.mocked(PluginModel.clear).mockResolvedValue(undefined); + await clientDB.insert(installedPlugins).values([ + { identifier: '123', type: 'plugin', userId }, + { identifier: '234', type: 'plugin', userId }, + ]); // Act - const result = await pluginService.removeAllPlugins(); + await pluginService.removeAllPlugins(); // Assert - expect(PluginModel.clear).toHaveBeenCalled(); - expect(result).toBe(undefined); + const result = await clientDB.query.installedPlugins.findMany({ + where: eq(installedPlugins.userId, userId), + }); + expect(result.length).toEqual(0); }); }); @@ -150,13 +160,17 @@ describe('PluginService', () => { // Arrange const id = 'plugin-id'; const settings = { color: 'blue' }; + await clientDB.insert(installedPlugins).values([{ identifier: id, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePluginSettings(id, settings); + await pluginService.updatePluginSettings(id, settings); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, { settings }); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, id), + }); + + expect(result).toMatchObject({ settings }); }); }); }); diff --git a/src/services/plugin/client.ts b/src/services/plugin/client.ts index c56f73119c14..1e534c5500e8 100644 --- a/src/services/plugin/client.ts +++ b/src/services/plugin/client.ts @@ -1,42 +1,52 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { PluginModel } from '@/database/_deprecated/models/plugin'; +import { clientDB } from '@/database/client/db'; +import { PluginModel } from '@/database/server/models/plugin'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { IPluginService, InstallPluginParams } from './type'; export class ClientService implements IPluginService { + private pluginModel: PluginModel; + + constructor(userId: string) { + this.pluginModel = new PluginModel(clientDB as any, userId); + } + installPlugin = async (plugin: InstallPluginParams) => { - return PluginModel.create(plugin); + await this.pluginModel.create(plugin); + return; }; getInstalledPlugins = () => { - return PluginModel.getList() as Promise; + return this.pluginModel.query() as Promise; }; - uninstallPlugin(identifier: string) { - return PluginModel.delete(identifier); + async uninstallPlugin(identifier: string) { + await this.pluginModel.delete(identifier); + return; } async createCustomPlugin(customPlugin: LobeToolCustomPlugin) { - return PluginModel.create({ ...customPlugin, type: 'customPlugin' }); + await this.pluginModel.create({ ...customPlugin, type: 'customPlugin' }); + return; } async updatePlugin(id: string, value: LobeToolCustomPlugin) { - await PluginModel.update(id, value); + await this.pluginModel.update(id, value); return; } async updatePluginManifest(id: string, manifest: LobeChatPluginManifest) { - await PluginModel.update(id, { manifest }); + await this.pluginModel.update(id, { manifest }); } async removeAllPlugins() { - return PluginModel.clear(); + await this.pluginModel.deleteAll(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async updatePluginSettings(id: string, settings: any, _?: AbortSignal) { - await PluginModel.update(id, { settings }); + await this.pluginModel.update(id, { settings }); } } diff --git a/src/services/plugin/index.ts b/src/services/plugin/index.ts index 77b3ab38869b..58265d316138 100644 --- a/src/services/plugin/index.ts +++ b/src/services/plugin/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const pluginService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/services/session/client.test.ts b/src/services/session/client.test.ts index 4a7274ce547a..95f83a167963 100644 --- a/src/services/session/client.test.ts +++ b/src/services/session/client.test.ts @@ -1,193 +1,130 @@ +import { eq, not } from 'drizzle-orm/expressions'; import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { SessionGroupModel } from '@/database/_deprecated/models/sessionGroup'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { + NewSession, + SessionItem, + agents, + agentsToSessions, + sessionGroups, + sessions, + users, +} from '@/database/schemas'; import { LobeAgentConfig } from '@/types/agent'; import { LobeAgentSession, LobeSessionType, SessionGroups } from '@/types/session'; import { ClientService } from './client'; -const sessionService = new ClientService(); - -// Mock the SessionModel -vi.mock('@/database/_deprecated/models/session', () => { - return { - SessionModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - clearTable: vi.fn(), - update: vi.fn(), - count: vi.fn(), - batchCreate: vi.fn(), - findById: vi.fn(), - isEmpty: vi.fn(), - queryByKeyword: vi.fn(), - updateConfig: vi.fn(), - queryByGroupIds: vi.fn(), - updatePinned: vi.fn(), - duplicate: vi.fn(), - queryWithGroups: vi.fn(), - }, - }; +const userId = 'message-db'; +const sessionService = new ClientService(userId); + +const mockSessionId = 'mock-session-id'; + +// Mock data +beforeEach(async () => { + await migrate(); + + // 在每个测试用例之前,清空表 + await clientDB.transaction(async (trx) => { + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + await trx.insert(sessions).values([{ id: mockSessionId, userId: userId }]); + await trx.insert(sessionGroups).values([ + { id: 'group-1', name: 'group-A', sort: 2, userId }, + { id: 'group-2', name: 'group-B', sort: 1, userId }, + { id: 'group-4', name: 'group-C', sort: 1, userId: '456' }, + ]); + }); }); -// Mock the SessionGroupModel -vi.mock('@/database/_deprecated/models/sessionGroup', () => { - return { - SessionGroupModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - clear: vi.fn(), - update: vi.fn(), - batchCreate: vi.fn(), - isEmpty: vi.fn(), - updateOrder: vi.fn(), - queryByKeyword: vi.fn(), - updateConfig: vi.fn(), - queryByGroupIds: vi.fn(), - }, - }; +afterEach(async () => { + // 在每个测试用例之后,清空表 + await clientDB.delete(users); }); describe('SessionService', () => { - const mockSessionId = 'mock-session-id'; const mockSession = { id: mockSessionId, type: 'agent', meta: { title: 'Mock Session' }, } as LobeAgentSession; - const mockSessions = [mockSession]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); describe('createSession', () => { it('should create a new session and return its id', async () => { // Setup const sessionType = LobeSessionType.Agent; const defaultValue = { meta: { title: 'New Session' } } as Partial; - (SessionModel.create as Mock).mockResolvedValue(mockSession); // Execute const sessionId = await sessionService.createSession(sessionType, defaultValue); // Assert - expect(SessionModel.create).toHaveBeenCalledWith(sessionType, defaultValue); - expect(sessionId).toBe(mockSessionId); - }); - - it('should throw an error if session creation fails', async () => { - // Setup - const sessionType = LobeSessionType.Agent; - const defaultValue = { meta: { title: 'New Session' } } as Partial; - (SessionModel.create as Mock).mockResolvedValue(null); - - // Execute & Assert - await expect(sessionService.createSession(sessionType, defaultValue)).rejects.toThrow( - 'session create Error', - ); - }); - }); - - describe('batchCreateSessions', () => { - it('should batch create sessions', async () => { - // Setup - (SessionModel.batchCreate as Mock).mockResolvedValue(mockSessions); - - // Execute - const result = await sessionService.batchCreateSessions(mockSessions); - - // Assert - expect(SessionModel.batchCreate).toHaveBeenCalledWith(mockSessions); - expect(result).toBe(mockSessions); - }); - }); - - describe('getSessionsByType', () => { - it('should retrieve sessions with their group ids', async () => { - // Setup - (SessionModel.query as Mock).mockResolvedValue(mockSessions); - - // Execute - const sessions = await sessionService.getSessionsByType(); - - // Assert - expect(SessionModel.query).toHaveBeenCalled(); - expect(sessions).toBe(mockSessions); - }); - - it('should retrieve all agent sessions', async () => { - // Setup - // Assuming that SessionModel.query has been modified to accept filters - const agentSessions = mockSessions.filter((session) => session.type === 'agent'); - (SessionModel.query as Mock).mockResolvedValue(agentSessions); - - // Execute - const result = await sessionService.getSessionsByType('agent'); - - // Assert - // Assuming that SessionModel.query would be called with a filter for agents - expect(SessionModel.query).toHaveBeenCalled(); // Add filter argument if applicable - expect(result).toBe(agentSessions); + expect(sessionId).toMatch(/^ssn_/); }); }); describe('removeSession', () => { it('should remove a session by its id', async () => { - // Setup - (SessionModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await sessionService.removeSession(mockSessionId); + await sessionService.removeSession(mockSessionId); + + // Assert + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); // Assert - expect(SessionModel.delete).toHaveBeenCalledWith(mockSessionId); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); }); describe('removeAllSessions', () => { it('should clear all sessions from the table', async () => { // Setup - (SessionModel.clearTable as Mock).mockResolvedValue(true); + await clientDB + .insert(sessions) + .values([{ userId: userId }, { userId: userId }, { userId: userId }]); // Execute - const result = await sessionService.removeAllSessions(); + await sessionService.removeAllSessions(); // Assert - expect(SessionModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); + const result = await clientDB.query.sessions.findMany({ + where: eq(sessionGroups.userId, userId), + }); + + expect(result.length).toBe(0); }); }); describe('updateSession', () => { - it('should update the group of a session', async () => { + it.skip('should update the group of a session', async () => { // Setup const groupId = 'new-group'; - (SessionModel.update as Mock).mockResolvedValue({ ...mockSession, group: groupId }); // Execute - const result = await sessionService.updateSession(mockSessionId, { group: groupId }); + await sessionService.updateSession(mockSessionId, { group: groupId }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { group: groupId }); - expect(result).toEqual({ ...mockSession, group: groupId }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + expect(result).toMatchObject({ group: groupId }); }); - it('should update the meta of a session', async () => { + it.skip('should update the meta of a session', async () => { // Setup const newMeta = { description: 'Updated description' }; - (SessionModel.update as Mock).mockResolvedValue({ ...mockSession, meta: newMeta }); // Execute - const result = await sessionService.updateSession(mockSessionId, { meta: newMeta }); + await sessionService.updateSession(mockSessionId, { meta: newMeta }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { meta: newMeta }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + expect(result).toEqual({ ...mockSession, meta: newMeta }); }); @@ -199,121 +136,109 @@ describe('SessionService', () => { await sessionService.updateSession(mockSessionId, { pinned }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { pinned: 1 }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + + expect(result!.pinned).toBeTruthy(); }); }); - describe('updateSessionConfig', () => { + describe.skip('updateSessionConfig', () => { it('should update the config of a session', async () => { // Setup const newConfig = { model: 'abc' } as LobeAgentConfig; - (SessionModel.updateConfig as Mock).mockResolvedValue({ ...mockSession, config: newConfig }); // Execute - const result = await sessionService.updateSessionConfig(mockSessionId, newConfig); + await sessionService.updateSessionConfig(mockSessionId, newConfig); // Assert - expect(SessionModel.updateConfig).toHaveBeenCalledWith(mockSessionId, newConfig); + const result = await sessionService.getSessionConfig(mockSessionId); expect(result).toEqual({ ...mockSession, config: newConfig }); }); }); describe('countSessions', () => { it('should return false if no sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(0); + await clientDB.delete(sessions); // Execute const result = await sessionService.countSessions(); // Assert - expect(SessionModel.count).toHaveBeenCalled(); expect(result).toBe(0); }); it('should return true if sessions exist', async () => { // Setup - (SessionModel.count as Mock).mockResolvedValue(1); + await clientDB.delete(sessions); + await clientDB.insert(sessions).values([{ userId }]); // Execute const result = await sessionService.countSessions(); // Assert - expect(SessionModel.count).toHaveBeenCalled(); expect(result).toBe(1); }); }); - describe('hasSessions', () => { - it('should return false if no sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(0); - - // Execute - const result = await sessionService.hasSessions(); - - // Assert - expect(SessionModel.count).toHaveBeenCalled(); - expect(result).toBe(false); - }); - - it('should return true if sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(1); - - // Execute - const result = await sessionService.hasSessions(); - - // Assert - expect(SessionModel.count).toHaveBeenCalled(); - expect(result).toBe(true); - }); - }); - describe('searchSessions', () => { it('should return sessions that match the keyword', async () => { // Setup - const keyword = 'search'; - (SessionModel.queryByKeyword as Mock).mockResolvedValue(mockSessions); + await clientDB.insert(agents).values({ userId, id: 'agent-1', title: 'Session Name' }); + await clientDB + .insert(agentsToSessions) + .values({ agentId: 'agent-1', sessionId: mockSessionId }); // Execute + const keyword = 'Name'; const result = await sessionService.searchSessions(keyword); // Assert - expect(SessionModel.queryByKeyword).toHaveBeenCalledWith(keyword); - expect(result).toBe(mockSessions); + // TODO: 后续需要把这个搜索的标题和描述都加上,现在这个 client 搜索会有问题 + expect(result).toMatchObject([{ id: mockSessionId }]); }); }); - describe('cloneSession', () => { + describe.skip('cloneSession', () => { it('should duplicate a session and return its id', async () => { // Setup const newTitle = 'Duplicated Session'; - (SessionModel.duplicate as Mock).mockResolvedValue({ - ...mockSession, + const session: NewSession = { id: 'duplicated-session-id', - }); + title: '123', + userId, + }; + await clientDB.insert(sessions).values([session]); + await clientDB.insert(agents).values({ userId, id: 'agent-1' }); + await clientDB + .insert(agentsToSessions) + .values({ agentId: 'agent-1', sessionId: 'duplicated-session-id' }); // Execute const duplicatedSessionId = await sessionService.cloneSession(mockSessionId, newTitle); // Assert - expect(SessionModel.duplicate).toHaveBeenCalledWith(mockSessionId, newTitle); - expect(duplicatedSessionId).toBe('duplicated-session-id'); + + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, duplicatedSessionId!), + }); + expect(result).toEqual({}); }); }); describe('getGroupedSessions', () => { it('should retrieve sessions with their group', async () => { - // Setup - (SessionModel.queryWithGroups as Mock).mockResolvedValue(mockSessions); - // Execute const sessionsWithGroup = await sessionService.getGroupedSessions(); - // Assert - expect(SessionModel.queryWithGroups).toHaveBeenCalled(); - expect(sessionsWithGroup).toBe(mockSessions); + expect(sessionsWithGroup).toMatchObject({ + sessionGroups: [ + { id: 'group-2', name: 'group-B', sort: 1 }, + { id: 'group-1', name: 'group-A', sort: 2 }, + ], + sessions: [{ id: 'mock-session-id', type: 'agent' }], + }); }); }); @@ -323,84 +248,66 @@ describe('SessionService', () => { // Setup const groupName = 'New Group'; const sort = 1; - (SessionGroupModel.create as Mock).mockResolvedValue({ - id: 'new-group-id', - name: groupName, - sort, - }); // Execute const groupId = await sessionService.createSessionGroup(groupName, sort); // Assert - expect(SessionGroupModel.create).toHaveBeenCalledWith(groupName, sort); - expect(groupId).toBe('new-group-id'); - }); - }); - - describe('batchCreateSessionGroups', () => { - it('should batch create session groups', async () => { - // Setup - const groups = [ - { id: 'group-1', name: 'Group 1', sort: 1 }, - { id: 'group-2', name: 'Group 2', sort: 2 }, - ] as SessionGroups; + expect(groupId).toMatch(/^sg_/); - (SessionGroupModel.batchCreate as Mock).mockResolvedValue(groups); - - // Execute - const result = await sessionService.batchCreateSessionGroups(groups); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); - // Assert - expect(SessionGroupModel.batchCreate).toHaveBeenCalledWith(groups); - expect(result).toBe(groups); + expect(result).toMatchObject({ id: groupId, name: groupName, sort }); }); }); describe('removeSessionGroup', () => { it('should remove a session group by its id', async () => { - // Setup - const removeChildren = true; - (SessionGroupModel.delete as Mock).mockResolvedValue(true); - + const groupId = 'group-1'; // Execute - const result = await sessionService.removeSessionGroup('group-id', removeChildren); + await sessionService.removeSessionGroup(groupId); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); // Assert - expect(SessionGroupModel.delete).toHaveBeenCalledWith('group-id', removeChildren); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); }); describe('clearSessionGroups', () => { it('should clear all session groups', async () => { - // Setup - (SessionGroupModel.clear as Mock).mockResolvedValue(true); - // Execute - const result = await sessionService.removeSessionGroups(); + await sessionService.removeSessionGroups(); // Assert - expect(SessionGroupModel.clear).toHaveBeenCalled(); - expect(result).toBe(true); + const result = await clientDB.query.sessionGroups.findMany({ + where: eq(sessionGroups.userId, userId), + }); + + expect(result.length).toBe(0); + + const result2 = await clientDB.query.sessionGroups.findMany({ + where: not(eq(sessionGroups.userId, userId)), + }); + + expect(result2.length).toBeGreaterThan(0); }); }); describe('getSessionGroups', () => { it('should retrieve all session groups', async () => { - // Setup - const groups = [ - { id: 'group-1', name: 'Group 1', sort: 1 }, - { id: 'group-2', name: 'Group 2', sort: 2 }, - ]; - (SessionGroupModel.query as Mock).mockResolvedValue(groups); - // Execute const result = await sessionService.getSessionGroups(); // Assert - expect(SessionGroupModel.query).toHaveBeenCalled(); - expect(result).toBe(groups); + const groups = [ + { id: 'group-2', name: 'group-B', sort: 1 }, + { id: 'group-1', name: 'group-A', sort: 2 }, + ]; + expect(result).toMatchObject(groups); }); }); @@ -409,14 +316,15 @@ describe('SessionService', () => { // Setup const groupId = 'group-1'; const data = { name: 'Updated Group', sort: 2 }; - (SessionGroupModel.update as Mock).mockResolvedValue({ id: groupId, ...data }); // Execute - const result = await sessionService.updateSessionGroup(groupId, data); + await sessionService.updateSessionGroup(groupId, data); // Assert - expect(SessionGroupModel.update).toHaveBeenCalledWith(groupId, data); - expect(result).toEqual({ id: groupId, ...data }); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); + expect(result).toMatchObject({ id: groupId, ...data }); }); }); @@ -427,14 +335,18 @@ describe('SessionService', () => { { id: 'group-1', sort: 2 }, { id: 'group-2', sort: 1 }, ]; - (SessionGroupModel.updateOrder as Mock).mockResolvedValue(true); // Execute - const result = await sessionService.updateSessionGroupOrder(sortMap); + await sessionService.updateSessionGroupOrder(sortMap); // Assert - expect(SessionGroupModel.updateOrder).toHaveBeenCalledWith(sortMap); - expect(result).toBe(true); + const data = await clientDB.query.sessionGroups.findMany({ + where: eq(sessionGroups.userId, userId), + }); + expect(data).toMatchObject([ + { id: 'group-1', sort: 2 }, + { id: 'group-2', sort: 1 }, + ]); }); }); }); diff --git a/src/services/session/client.ts b/src/services/session/client.ts index 2d205cca8c8c..3592bc1f4691 100644 --- a/src/services/session/client.ts +++ b/src/services/session/client.ts @@ -1,9 +1,11 @@ import { DeepPartial } from 'utility-types'; import { INBOX_SESSION_ID } from '@/const/session'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { SessionGroupModel } from '@/database/_deprecated/models/sessionGroup'; import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB } from '@/database/client/db'; +import { AgentItem } from '@/database/schemas'; +import { SessionModel } from '@/database/server/models/session'; +import { SessionGroupModel } from '@/database/server/models/sessionGroup'; import { useUserStore } from '@/store/user'; import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent'; import { MetaData } from '@/types/meta'; @@ -20,11 +22,22 @@ import { merge } from '@/utils/merge'; import { ISessionService } from './type'; export class ClientService implements ISessionService { - async createSession( - type: LobeSessionType, - defaultValue: Partial, - ): Promise { - const item = await SessionModel.create(type, defaultValue); + private sessionModel: SessionModel; + private sessionGroupModel: SessionGroupModel; + + constructor(userId: string) { + this.sessionGroupModel = new SessionGroupModel(clientDB as any, userId); + this.sessionModel = new SessionModel(clientDB as any, userId); + } + + async createSession(type: LobeSessionType, data: Partial): Promise { + const { config, group, meta, ...session } = data; + + const item = await this.sessionModel.create({ + config: { ...config, ...meta } as any, + session: { ...session, groupId: group }, + type, + }); if (!item) { throw new Error('session create Error'); } @@ -32,17 +45,18 @@ export class ClientService implements ISessionService { } async batchCreateSessions(importSessions: LobeSessions) { - return SessionModel.batchCreate(importSessions); + // @ts-ignore + return this.sessionModel.batchCreate(importSessions); } async cloneSession(id: string, newTitle: string): Promise { - const res = await SessionModel.duplicate(id, newTitle); + const res = await this.sessionModel.duplicate(id, newTitle); if (res) return res?.id; } async getGroupedSessions(): Promise { - return SessionModel.queryWithGroups(); + return this.sessionModel.queryWithGroups(); } async getSessionConfig(id: string): Promise { @@ -50,54 +64,50 @@ export class ClientService implements ISessionService { return UserModel.getAgentConfig(); } - const res = await SessionModel.findById(id); + const res = await this.sessionModel.findByIdOrSlug(id); if (!res) throw new Error('Session not found'); - return res.config as LobeAgentConfig; + return res.agent as LobeAgentConfig; } + /** + * 这个方法要对应移除的 + */ async getSessionsByType(type: 'agent' | 'group' | 'all' = 'all'): Promise { switch (type) { // TODO: add a filter to get only agents or agents case 'group': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } case 'agent': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } case 'all': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } } } - async getAllAgents(): Promise { - // TODO: add a filter to get only agents - return await SessionModel.query(); - } - async countSessions() { - return SessionModel.count(); - } - - async hasSessions() { - return (await this.countSessions()) !== 0; + return this.sessionModel.count(); } async searchSessions(keyword: string) { - return SessionModel.queryByKeyword(keyword); + return this.sessionModel.queryByKeyword(keyword); } async updateSession( id: string, data: Partial>, ) { - const pinned = typeof data.pinned === 'boolean' ? (data.pinned ? 1 : 0) : undefined; - const prev = await SessionModel.findById(id); + const prev = await this.sessionModel.findByIdOrSlug(id); - return SessionModel.update(id, merge(prev, { ...data, pinned })); + return this.sessionModel.update(id, merge(prev, data)); } async updateSessionConfig( @@ -112,7 +122,7 @@ export class ClientService implements ISessionService { return useUserStore.getState().updateDefaultAgent({ config }); } - return SessionModel.updateConfig(activeId, config); + return this.sessionModel.updateConfig(activeId, config as AgentItem); } async updateSessionMeta( @@ -124,7 +134,7 @@ export class ClientService implements ISessionService { // inbox 不允许修改 meta if (activeId === INBOX_SESSION_ID) return; - return SessionModel.update(activeId, { meta }); + return this.sessionModel.update(activeId, meta); } async updateSessionChatConfig( @@ -137,11 +147,11 @@ export class ClientService implements ISessionService { } async removeSession(id: string) { - return SessionModel.delete(id); + return this.sessionModel.delete(id); } async removeAllSessions() { - return SessionModel.clearTable(); + return this.sessionModel.deleteAll(); } // ************************************** // @@ -149,7 +159,7 @@ export class ClientService implements ISessionService { // ************************************** // async createSessionGroup(name: string, sort?: number) { - const item = await SessionGroupModel.create(name, sort); + const item = await this.sessionGroupModel.create({ name, sort }); if (!item) { throw new Error('session group create Error'); } @@ -157,27 +167,28 @@ export class ClientService implements ISessionService { return item.id; } - async batchCreateSessionGroups(groups: SessionGroups) { - return SessionGroupModel.batchCreate(groups); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async batchCreateSessionGroups(_groups: SessionGroups) { + return { added: 0, ids: [], skips: [], success: true }; } - async removeSessionGroup(id: string, removeChildren?: boolean) { - return await SessionGroupModel.delete(id, removeChildren); + async removeSessionGroup(id: string) { + return await this.sessionGroupModel.delete(id); } async updateSessionGroup(id: string, data: Partial) { - return SessionGroupModel.update(id, data); + return this.sessionGroupModel.update(id, data); } async updateSessionGroupOrder(sortMap: { id: string; sort: number }[]) { - return SessionGroupModel.updateOrder(sortMap); + return this.sessionGroupModel.updateOrder(sortMap); } async getSessionGroups(): Promise { - return SessionGroupModel.query(); + return this.sessionGroupModel.query(); } async removeSessionGroups() { - return SessionGroupModel.clear(); + return this.sessionGroupModel.deleteAll(); } } diff --git a/src/services/session/index.ts b/src/services/session/index.ts index 0d55e3cc45e5..34f8d766a500 100644 --- a/src/services/session/index.ts +++ b/src/services/session/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const sessionService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/services/session/type.ts b/src/services/session/type.ts index f51f2f39780d..59528fe138ca 100644 --- a/src/services/session/type.ts +++ b/src/services/session/type.ts @@ -16,13 +16,21 @@ import { export interface ISessionService { createSession(type: LobeSessionType, defaultValue: Partial): Promise; + + /** + * 需要废弃 + * @deprecated + */ batchCreateSessions(importSessions: LobeSessions): Promise; cloneSession(id: string, newTitle: string): Promise; getGroupedSessions(): Promise; + + /** + * @deprecated + */ getSessionsByType(type: 'agent' | 'group' | 'all'): Promise; countSessions(): Promise; - hasSessions(): Promise; searchSessions(keyword: string): Promise; updateSession( @@ -53,6 +61,11 @@ export interface ISessionService { // ************************************** // createSessionGroup(name: string, sort?: number): Promise; + + /** + * 需要废弃 + * @deprecated + */ batchCreateSessionGroups(groups: SessionGroups): Promise; getSessionGroups(): Promise; diff --git a/src/services/topic/client.test.ts b/src/services/topic/client.test.ts index 211abefa7d36..ae943c2b6d7b 100644 --- a/src/services/topic/client.test.ts +++ b/src/services/topic/client.test.ts @@ -1,75 +1,66 @@ -import { Mock, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { CreateTopicParams, TopicModel } from '@/database/_deprecated/models/topic'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { sessions, topics, users } from '@/database/schemas'; import { ChatTopic } from '@/types/topic'; import { ClientService } from './client'; -const topicService = new ClientService(); -// Mock the TopicModel -vi.mock('@/database/_deprecated/models/topic', () => { - return { - TopicModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - count: vi.fn(), - batchDeleteBySessionId: vi.fn(), - batchDelete: vi.fn(), - clearTable: vi.fn(), - toggleFavorite: vi.fn(), - batchCreate: vi.fn(), - update: vi.fn(), - queryAll: vi.fn(), - queryByKeyword: vi.fn(), - }, - }; -}); +// Mock data +const userId = 'topic-user-test'; +const sessionId = 'topic-session'; +const mockTopicId = 'mock-topic-id'; -describe('TopicService', () => { - // Mock data - const mockTopicId = 'mock-topic-id'; - const mockTopic: ChatTopic = { - createdAt: 100, - updatedAt: 100, - id: mockTopicId, - title: 'Mock Topic', - }; - const mockTopics = [mockTopic]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); +const mockTopic = { + id: mockTopicId, + title: 'Mock Topic', +}; + +const topicService = new ClientService(userId); + +beforeEach(async () => { + // Reset all mocks before running each test case + vi.resetAllMocks(); + + await migrate(); + + await clientDB.delete(users); + + // 创建测试数据 + await clientDB.transaction(async (tx) => { + await tx.insert(users).values({ id: userId }); + await tx.insert(sessions).values({ id: sessionId, userId }); + await tx.insert(topics).values({ ...mockTopic, sessionId, userId }); }); +}); +describe('TopicService', () => { describe('createTopic', () => { it('should create a topic and return its id', async () => { // Setup - const createParams: CreateTopicParams = { + const createParams = { title: 'New Topic', - sessionId: '1', + sessionId: sessionId, }; - (TopicModel.create as Mock).mockResolvedValue(mockTopic); // Execute const topicId = await topicService.createTopic(createParams); // Assert - expect(TopicModel.create).toHaveBeenCalledWith(createParams); - expect(topicId).toBe(mockTopicId); + expect(topicId).toBeDefined(); }); + it('should throw an error if topic creation fails', async () => { // Setup - const createParams: CreateTopicParams = { + const createParams = { title: 'New Topic', - sessionId: '1', + sessionId: 123 as any, // sessionId should be string }; - (TopicModel.create as Mock).mockResolvedValue(null); - // Execute & Assert - await expect(topicService.createTopic(createParams)).rejects.toThrow('topic create Error'); + await expect(topicService.createTopic(createParams)).rejects.toThrowError(); }); }); @@ -77,56 +68,46 @@ describe('TopicService', () => { // Example for getTopics it('should query topics with given parameters', async () => { // Setup - const queryParams = { sessionId: 'session-id' }; - (TopicModel.query as Mock).mockResolvedValue(mockTopics); + const queryParams = { sessionId }; // Execute - const topics = await topicService.getTopics(queryParams); + const data = await topicService.getTopics(queryParams); // Assert - expect(TopicModel.query).toHaveBeenCalledWith(queryParams); - expect(topics).toBe(mockTopics); + expect(data[0]).toMatchObject(mockTopic); }); }); describe('updateTopic', () => { // Example for updateFavorite it('should toggle favorite status of a topic', async () => { - // Setup - const newState = true; - // Execute - await topicService.updateTopic(mockTopicId, { favorite: newState }); + const result = await topicService.updateTopic(mockTopicId, { favorite: true }); // Assert - expect(TopicModel.update).toHaveBeenCalledWith(mockTopicId, { favorite: 1 }); + expect(result[0].favorite).toBeTruthy(); }); it('should update the title of a topic', async () => { // Setup const newTitle = 'Updated Topic Title'; - (TopicModel.update as Mock).mockResolvedValue({ ...mockTopic, title: newTitle }); // Execute const result = await topicService.updateTopic(mockTopicId, { title: newTitle }); // Assert - expect(TopicModel.update).toHaveBeenCalledWith(mockTopicId, { title: newTitle }); - expect(result).toEqual({ ...mockTopic, title: newTitle }); + expect(result[0].title).toEqual(newTitle); }); }); describe('removeTopic', () => { it('should remove a topic by id', async () => { - // Setup - (TopicModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await topicService.removeTopic(mockTopicId); + await topicService.removeTopic(mockTopicId); + const result = await clientDB.query.topics.findFirst({ where: eq(topics.id, mockTopicId) }); // Assert - expect(TopicModel.delete).toHaveBeenCalledWith(mockTopicId); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); }); @@ -134,111 +115,101 @@ describe('TopicService', () => { it('should remove all topics with a given session id', async () => { // Setup const sessionId = 'session-id'; - (TopicModel.batchDeleteBySessionId as Mock).mockResolvedValue(true); // Execute - const result = await topicService.removeTopics(sessionId); + await topicService.removeTopics(sessionId); + const result = await clientDB.query.topics.findMany({ + where: eq(topics.sessionId, sessionId), + }); - // Assert - expect(TopicModel.batchDeleteBySessionId).toHaveBeenCalledWith(sessionId); - expect(result).toBe(true); + expect(result.length).toEqual(0); }); }); describe('batchRemoveTopics', () => { it('should batch remove topics', async () => { + await clientDB.insert(topics).values([{ id: 'topic-id-1', title: 'topic-title', userId }]); // Setup const topicIds = [mockTopicId, 'another-topic-id']; - (TopicModel.batchDelete as Mock).mockResolvedValue(true); // Execute - const result = await topicService.batchRemoveTopics(topicIds); + await topicService.batchRemoveTopics(topicIds); + + const count = await clientDB.$count(topics); // Assert - expect(TopicModel.batchDelete).toHaveBeenCalledWith(topicIds); - expect(result).toBe(true); + expect(count).toBe(1); }); }); describe('removeAllTopic', () => { it('should clear all topics from the table', async () => { - // Setup - (TopicModel.clearTable as Mock).mockResolvedValue(true); - // Execute - const result = await topicService.removeAllTopic(); + await topicService.removeAllTopic(); + const count = await clientDB.$count(topics); // Assert - expect(TopicModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); + expect(count).toBe(0); }); }); describe('batchCreateTopics', () => { it('should batch create topics', async () => { - // Setup - (TopicModel.batchCreate as Mock).mockResolvedValue(mockTopics); - // Execute - const result = await topicService.batchCreateTopics(mockTopics); + const result = await topicService.batchCreateTopics([ + { id: 'topic-id-1', title: 'topic-title' }, + { id: 'topic-id-2', title: 'topic-title' }, + ] as ChatTopic[]); // Assert - expect(TopicModel.batchCreate).toHaveBeenCalledWith(mockTopics); - expect(result).toBe(mockTopics); + expect(result.success).toBeTruthy(); + expect(result.added).toBe(2); }); }); describe('getAllTopics', () => { it('should retrieve all topics', async () => { - // Setup - (TopicModel.queryAll as Mock).mockResolvedValue(mockTopics); - + await clientDB.insert(topics).values([ + { id: 'topic-id-1', title: 'topic-title', userId }, + { id: 'topic-id-2', title: 'topic-title', userId }, + ]); // Execute const result = await topicService.getAllTopics(); // Assert - expect(TopicModel.queryAll).toHaveBeenCalled(); - expect(result).toBe(mockTopics); + expect(result.length).toEqual(3); }); }); describe('searchTopics', () => { it('should return all topics that match the keyword', async () => { // Setup - const keyword = 'search'; - (TopicModel.queryByKeyword as Mock).mockResolvedValue(mockTopics); + const keyword = 'Topic'; // Execute - const result = await topicService.searchTopics(keyword, undefined); + const result = await topicService.searchTopics(keyword, sessionId); // Assert - expect(TopicModel.queryByKeyword).toHaveBeenCalledWith(keyword, undefined); - expect(result).toBe(mockTopics); + expect(result.length).toEqual(1); }); - }); - - describe('countTopics', () => { - it('should return false if no topics exist', async () => { + it('should return empty topic if not match the keyword', async () => { // Setup - (TopicModel.count as Mock).mockResolvedValue(0); + const keyword = 'search'; // Execute - const result = await topicService.countTopics(); + const result = await topicService.searchTopics(keyword, sessionId); // Assert - expect(TopicModel.count).toHaveBeenCalled(); - expect(result).toBe(0); + expect(result.length).toEqual(0); }); + }); - it('should return true if topics exist', async () => { - // Setup - (TopicModel.count as Mock).mockResolvedValue(1); - + describe('countTopics', () => { + it('should return topic counts', async () => { // Execute const result = await topicService.countTopics(); // Assert - expect(TopicModel.count).toHaveBeenCalled(); expect(result).toBe(1); }); }); diff --git a/src/services/topic/client.ts b/src/services/topic/client.ts index eeb2ffa2e395..337b8bcb2bba 100644 --- a/src/services/topic/client.ts +++ b/src/services/topic/client.ts @@ -1,11 +1,17 @@ -import { TopicModel } from '@/database/_deprecated/models/topic'; +import { clientDB } from '@/database/client/db'; +import { TopicModel } from '@/database/server/models/topic'; import { ChatTopic } from '@/types/topic'; import { CreateTopicParams, ITopicService, QueryTopicParams } from './type'; export class ClientService implements ITopicService { + private topicModel: TopicModel; + constructor(userId: string) { + this.topicModel = new TopicModel(clientDB as any, userId); + } + async createTopic(params: CreateTopicParams): Promise { - const item = await TopicModel.create(params as any); + const item = await this.topicModel.create(params as any); if (!item) { throw new Error('topic create Error'); @@ -15,56 +21,54 @@ export class ClientService implements ITopicService { } async batchCreateTopics(importTopics: ChatTopic[]) { - return TopicModel.batchCreate(importTopics as any); + const data = await this.topicModel.batchCreate(importTopics as any); + + return { added: data.length, ids: [], skips: [], success: true }; } async cloneTopic(id: string, newTitle?: string) { - return TopicModel.duplicateTopic(id, newTitle); + const data = await this.topicModel.duplicate(id, newTitle); + return data.topic.id; } - async getTopics(params: QueryTopicParams): Promise { - return TopicModel.query(params); + async getTopics(params: QueryTopicParams) { + const data = await this.topicModel.query(params); + return data as unknown as Promise; } async searchTopics(keyword: string, sessionId?: string) { - return TopicModel.queryByKeyword(keyword, sessionId); - } + const data = await this.topicModel.queryByKeyword(keyword, sessionId); - async getAllTopics() { - return TopicModel.queryAll(); + return data as unknown as Promise; } - async countTopics() { - return TopicModel.count(); - } + async getAllTopics() { + const data = await this.topicModel.queryAll(); - async updateTopicFavorite(id: string, favorite?: boolean) { - return this.updateTopic(id, { favorite }); + return data as unknown as Promise; } - async updateTopicTitle(id: string, text: string) { - return this.updateTopic(id, { title: text }); + async countTopics() { + return this.topicModel.count(); } async updateTopic(id: string, data: Partial) { - const favorite = typeof data.favorite !== 'undefined' ? (data.favorite ? 1 : 0) : undefined; - - return TopicModel.update(id, { ...data, favorite }); + return this.topicModel.update(id, data as any); } async removeTopic(id: string) { - return TopicModel.delete(id); + return this.topicModel.delete(id); } async removeTopics(sessionId: string) { - return TopicModel.batchDeleteBySessionId(sessionId); + return this.topicModel.batchDeleteBySessionId(sessionId); } async batchRemoveTopics(topics: string[]) { - return TopicModel.batchDelete(topics); + return this.topicModel.batchDelete(topics); } async removeAllTopic() { - return TopicModel.clearTable(); + return this.topicModel.deleteAll(); } } diff --git a/src/services/topic/index.ts b/src/services/topic/index.ts index 360656149ea5..1c9330e9a499 100644 --- a/src/services/topic/index.ts +++ b/src/services/topic/index.ts @@ -1,6 +1,7 @@ - import { ClientService } from './client'; import { ServerService } from './server'; export const topicService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/services/upload.ts b/src/services/upload.ts index 38f6cdd2959b..64fec769ecad 100644 --- a/src/services/upload.ts +++ b/src/services/upload.ts @@ -1,7 +1,8 @@ import { fileEnv } from '@/config/file'; import { edgeClient } from '@/libs/trpc/client'; import { API_ENDPOINTS } from '@/services/_url'; -import { FileMetadata, UploadFileParams } from '@/types/files'; +import { clientS3Storage } from '@/services/file/ClientS3'; +import { FileMetadata } from '@/types/files'; import { FileUploadState, FileUploadStatus } from '@/types/files/upload'; import { uuid } from '@/utils/uuid'; @@ -66,23 +67,14 @@ class UploadService { return result; }; - uploadToClientDB = async (params: UploadFileParams, file: File) => { - const { FileModel } = await import('@/database/_deprecated/models/file'); - const fileArrayBuffer = await file.arrayBuffer(); - - // save to local storage - // we may want to save to a remote server later - const res = await FileModel.create({ - createdAt: Date.now(), - ...params, - data: fileArrayBuffer, - }); - // arrayBuffer to url - const base64 = Buffer.from(fileArrayBuffer).toString('base64'); + uploadToClientS3 = async (hash: string, file: File): Promise => { + await clientS3Storage.putObject(hash, file); return { - id: res.id, - url: `data:${params.fileType};base64,${base64}`, + date: (Date.now() / 1000 / 60 / 60).toFixed(0), + dirname: '', + filename: file.name, + path: `client-s3://${hash}`, }; }; diff --git a/src/services/user/client.test.ts b/src/services/user/client.test.ts index f79f8294a2f5..3887518ed28a 100644 --- a/src/services/user/client.test.ts +++ b/src/services/user/client.test.ts @@ -1,22 +1,15 @@ +import { eq } from 'drizzle-orm'; import { DeepPartial } from 'utility-types'; -import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { userSettings, users } from '@/database/schemas'; import { UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; -import { AsyncLocalStorage } from '@/utils/localStorage'; import { ClientService } from './client'; -vi.mock('@/database/_deprecated/models/user', () => ({ - UserModel: { - getUser: vi.fn(), - updateSettings: vi.fn(), - resetSettings: vi.fn(), - updateAvatar: vi.fn(), - }, -})); - const mockUser = { avatar: 'avatar.png', settings: { themeMode: 'light' } as unknown as UserSettings, @@ -26,63 +19,70 @@ const mockUser = { const mockPreference = { useCmdEnterToSend: true, } as UserPreference; +const clientService = new ClientService(mockUser.uuid); -describe('ClientService', () => { - let clientService: ClientService; +beforeEach(async () => { + vi.clearAllMocks(); - beforeEach(() => { - vi.clearAllMocks(); - clientService = new ClientService(); - }); + await migrate(); + + await clientDB.insert(users).values({ id: mockUser.uuid, avatar: 'avatar.png' }); + await clientDB + .insert(userSettings) + .values({ id: mockUser.uuid, general: { themeMode: 'light' } }); +}); + +afterEach(async () => { + await clientDB.delete(users); +}); +describe('ClientService', () => { it('should get user state correctly', async () => { - (UserModel.getUser as Mock).mockResolvedValue(mockUser); const spyOn = vi .spyOn(clientService['preferenceStorage'], 'getFromLocalStorage') .mockResolvedValue(mockPreference); const userState = await clientService.getUserState(); - expect(userState).toEqual({ + expect(userState).toMatchObject({ avatar: mockUser.avatar, isOnboard: true, canEnablePWAGuide: false, hasConversation: false, canEnableTrace: false, preference: mockPreference, - settings: mockUser.settings, + settings: { general: { themeMode: 'light' } }, userId: mockUser.uuid, }); - expect(UserModel.getUser).toHaveBeenCalledTimes(1); expect(spyOn).toHaveBeenCalledTimes(1); }); it('should update user settings correctly', async () => { const settingsPatch: DeepPartial = { general: { themeMode: 'dark' } }; - (UserModel.updateSettings as Mock).mockResolvedValue(undefined); await clientService.updateUserSettings(settingsPatch); - expect(UserModel.updateSettings).toHaveBeenCalledWith(settingsPatch); - expect(UserModel.updateSettings).toHaveBeenCalledTimes(1); + const result = await clientDB.query.userSettings.findFirst({ + where: eq(userSettings.id, mockUser.uuid), + }); + + expect(result).toMatchObject(settingsPatch); }); it('should reset user settings correctly', async () => { - (UserModel.resetSettings as Mock).mockResolvedValue(undefined); - await clientService.resetUserSettings(); - expect(UserModel.resetSettings).toHaveBeenCalledTimes(1); + const result = await clientDB.query.userSettings.findFirst({ + where: eq(userSettings.id, mockUser.uuid), + }); + + expect(result).toBeUndefined(); }); it('should update user avatar correctly', async () => { const newAvatar = 'new-avatar.png'; - (UserModel.updateAvatar as Mock).mockResolvedValue(undefined); await clientService.updateAvatar(newAvatar); - - expect(UserModel.updateAvatar).toHaveBeenCalledWith(newAvatar); - expect(UserModel.updateAvatar).toHaveBeenCalledTimes(1); }); it('should update user preference correctly', async () => { diff --git a/src/services/user/client.ts b/src/services/user/client.ts index 500d856a11b5..a38c3399173e 100644 --- a/src/services/user/client.ts +++ b/src/services/user/client.ts @@ -1,8 +1,9 @@ import { DeepPartial } from 'utility-types'; -import { MessageModel } from '@/database/_deprecated/models/message'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB } from '@/database/client/db'; +import { MessageModel } from '@/database/server/models/message'; +import { SessionModel } from '@/database/server/models/session'; +import { UserModel } from '@/database/server/models/user'; import { UserGuide, UserInitializationState, UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; import { AsyncLocalStorage } from '@/utils/localStorage'; @@ -11,39 +12,46 @@ import { IUserService } from './type'; export class ClientService implements IUserService { private preferenceStorage: AsyncLocalStorage; + private userModel: UserModel; + private messageModel: MessageModel; + private sessionModel: SessionModel; + private userId: string; - constructor() { + constructor(userId: string) { this.preferenceStorage = new AsyncLocalStorage('LOBE_PREFERENCE'); + this.userModel = new UserModel(clientDB as any, userId); + this.userId = userId; + this.messageModel = new MessageModel(clientDB as any, userId); + this.sessionModel = new SessionModel(clientDB as any, userId); } async getUserState(): Promise { - const user = await UserModel.getUser(); - const messageCount = await MessageModel.count(); - const sessionCount = await SessionModel.count(); + const state = await this.userModel.getUserState(); + const user = await UserModel.findById(clientDB as any, this.userId); + const messageCount = await this.messageModel.count(); + const sessionCount = await this.sessionModel.count(); return { - avatar: user.avatar, + ...state, + avatar: user?.avatar as string, canEnablePWAGuide: messageCount >= 4, canEnableTrace: messageCount >= 4, hasConversation: messageCount > 0 || sessionCount > 0, isOnboard: true, preference: await this.preferenceStorage.getFromLocalStorage(), - settings: user.settings as UserSettings, - userId: user.uuid, }; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateUserSettings = async (patch: DeepPartial, _?: any) => { - return UserModel.updateSettings(patch); + updateUserSettings = async (patch: DeepPartial) => { + return this.userModel.updateSetting(patch); }; resetUserSettings = async () => { - return UserModel.resetSettings(); + return this.userModel.deleteSetting(); }; updateAvatar(avatar: string) { - return UserModel.updateAvatar(avatar); + return this.userModel.updateUser({ avatar }); } async updatePreference(preference: Partial) { diff --git a/src/services/user/index.ts b/src/services/user/index.ts index e472bdaef2c0..80272a68900a 100644 --- a/src/services/user/index.ts +++ b/src/services/user/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const userService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/store/chat/slices/builtinTool/action.test.ts b/src/store/chat/slices/builtinTool/action.test.ts index 795806b44195..002e15230027 100644 --- a/src/store/chat/slices/builtinTool/action.test.ts +++ b/src/store/chat/slices/builtinTool/action.test.ts @@ -39,7 +39,7 @@ describe('chatToolSlice', () => { vi.spyOn(uploadService, 'getImageFileByUrlWithCORS').mockResolvedValue( new File(['1'], 'file.png', { type: 'image/png' }), ); - vi.spyOn(uploadService, 'uploadToClientDB').mockResolvedValue({} as any); + vi.spyOn(uploadService, 'uploadToClientS3').mockResolvedValue({} as any); vi.spyOn(fileService, 'createFile').mockResolvedValue({ id: mockId, url: '' }); vi.spyOn(result.current, 'toggleDallEImageLoading'); @@ -48,7 +48,7 @@ describe('chatToolSlice', () => { }); // For each prompt, loading is toggled on and then off expect(imageGenerationService.generateImage).toHaveBeenCalledTimes(prompts.length); - expect(uploadService.uploadToClientDB).toHaveBeenCalledTimes(prompts.length); + expect(uploadService.uploadToClientS3).toHaveBeenCalledTimes(prompts.length); expect(result.current.toggleDallEImageLoading).toHaveBeenCalledTimes(prompts.length * 2); }); diff --git a/src/store/file/slices/upload/action.ts b/src/store/file/slices/upload/action.ts index 1134d7907f43..3f73de77b08b 100644 --- a/src/store/file/slices/upload/action.ts +++ b/src/store/file/slices/upload/action.ts @@ -6,14 +6,11 @@ import { message } from '@/components/AntdStaticMethods'; import { LOBE_CHAT_CLOUD } from '@/const/branding'; import { isServerMode } from '@/const/version'; import { fileService } from '@/services/file'; -import { ServerService } from '@/services/file/server'; import { uploadService } from '@/services/upload'; import { FileMetadata, UploadFileItem } from '@/types/files'; import { FileStore } from '../../store'; -const serverFileService = new ServerService(); - interface UploadWithProgressParams { file: File; knowledgeBaseId?: string; @@ -43,10 +40,6 @@ interface UploadWithProgressResult { } export interface FileUploadAction { - internal_uploadToClientDB: ( - params: Omit, - ) => Promise; - internal_uploadToServer: (params: UploadWithProgressParams) => Promise; uploadWithProgress: ( params: UploadWithProgressParams, ) => Promise; @@ -57,51 +50,14 @@ export const createFileUploadSlice: StateCreator< [['zustand/devtools', never]], [], FileUploadAction -> = (set, get) => ({ - internal_uploadToClientDB: async ({ file, onStatusUpdate, skipCheckFileType }) => { - if (!skipCheckFileType && !file.type.startsWith('image')) { - onStatusUpdate?.({ id: file.name, type: 'removeFile' }); - message.info({ - content: t('upload.fileOnlySupportInServerMode', { - cloud: LOBE_CHAT_CLOUD, - ext: file.name.split('.').pop(), - ns: 'error', - }), - duration: 5, - }); - return; - } - - const fileArrayBuffer = await file.arrayBuffer(); - - const hash = sha256(fileArrayBuffer); - - const data = await uploadService.uploadToClientDB( - { fileType: file.type, hash, name: file.name, saveMode: 'local', size: file.size }, - file, - ); - - onStatusUpdate?.({ - id: file.name, - type: 'updateFile', - value: { - fileUrl: data.url, - id: data.id, - status: 'success', - uploadState: { progress: 100, restTime: 0, speed: 0 }, - }, - }); - - return data; - }, - - internal_uploadToServer: async ({ file, onStatusUpdate, knowledgeBaseId }) => { +> = () => ({ + uploadWithProgress: async ({ file, onStatusUpdate, knowledgeBaseId, skipCheckFileType }) => { const fileArrayBuffer = await file.arrayBuffer(); // 1. check file hash const hash = sha256(fileArrayBuffer); - const checkStatus = await serverFileService.checkFileHash(hash); + const checkStatus = await fileService.checkFileHash(hash); let metadata: FileMetadata; // 2. if file exist, just skip upload @@ -112,17 +68,37 @@ export const createFileUploadSlice: StateCreator< type: 'updateFile', value: { status: 'processing', uploadState: { progress: 100, restTime: 0, speed: 0 } }, }); - } else { - // 2. if file don't exist, need upload files - metadata = await uploadService.uploadWithProgress(file, { - onProgress: (status, upload) => { - onStatusUpdate?.({ - id: file.name, - type: 'updateFile', - value: { status: status === 'success' ? 'processing' : status, uploadState: upload }, + } + // 2. if file don't exist, need upload files + else { + // if is server mode, upload to server s3, or upload to client s3 + if (isServerMode) { + metadata = await uploadService.uploadWithProgress(file, { + onProgress: (status, upload) => { + onStatusUpdate?.({ + id: file.name, + type: 'updateFile', + value: { status: status === 'success' ? 'processing' : status, uploadState: upload }, + }); + }, + }); + } else { + if (!skipCheckFileType && !file.type.startsWith('image')) { + onStatusUpdate?.({ id: file.name, type: 'removeFile' }); + message.info({ + content: t('upload.fileOnlySupportInServerMode', { + cloud: LOBE_CHAT_CLOUD, + ext: file.name.split('.').pop(), + ns: 'error', + }), + duration: 5, }); - }, - }); + return; + } + + // Upload to the indexeddb in the browser + metadata = await uploadService.uploadToClientS3(hash, file); + } } // 3. use more powerful file type detector to get file type @@ -138,12 +114,10 @@ export const createFileUploadSlice: StateCreator< // 4. create file to db const data = await fileService.createFile( { - createdAt: Date.now(), fileType, hash, metadata, name: file.name, - saveMode: 'url', size: file.size, url: metadata.path, }, @@ -163,12 +137,4 @@ export const createFileUploadSlice: StateCreator< return data; }, - - uploadWithProgress: async (payload) => { - const { internal_uploadToServer, internal_uploadToClientDB } = get(); - - if (isServerMode) return internal_uploadToServer(payload); - - return internal_uploadToClientDB(payload); - }, }); diff --git a/src/store/session/slices/sessionGroup/reducer.test.ts b/src/store/session/slices/sessionGroup/reducer.test.ts index 17a947ca217a..52fe51b9e329 100644 --- a/src/store/session/slices/sessionGroup/reducer.test.ts +++ b/src/store/session/slices/sessionGroup/reducer.test.ts @@ -10,14 +10,14 @@ describe('sessionGroupsReducer', () => { { id: nanoid(), name: 'Group 1', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), }, { id: nanoid(), name: 'Group 2', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), sort: 1, }, ]; @@ -26,8 +26,8 @@ describe('sessionGroupsReducer', () => { const newItem: SessionGroupItem = { id: nanoid(), name: 'New Group', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), }; const result = sessionGroupsReducer(initialState, { diff --git a/src/store/user/slices/common/action.ts b/src/store/user/slices/common/action.ts index 5f523d8d7dde..1952e7c6e5e3 100644 --- a/src/store/user/slices/common/action.ts +++ b/src/store/user/slices/common/action.ts @@ -48,7 +48,8 @@ export const createCommonSlice: StateCreator< updateAvatar: async (avatar) => { const { ClientService } = await import('@/services/user/client'); - const clientService = new ClientService(); + // TODO: add userId + const clientService = new ClientService(''); await clientService.updateAvatar(avatar); await get().refreshUserState(); diff --git a/src/types/files/upload.ts b/src/types/files/upload.ts index 83aa3d5be9ce..e9d0884ad6d1 100644 --- a/src/types/files/upload.ts +++ b/src/types/files/upload.ts @@ -53,7 +53,6 @@ export const FileMetadataSchema = z.object({ export type FileMetadata = z.infer; export const UploadFileSchema = z.object({ - data: z.instanceof(ArrayBuffer).optional(), /** * file type * @example 'image/png' @@ -77,7 +76,6 @@ export const UploadFileSchema = z.object({ * local mean save the raw file into data * url mean upload the file to a cdn and then save the url */ - saveMode: z.enum(['local', 'url']), /** * file size */ @@ -89,3 +87,11 @@ export const UploadFileSchema = z.object({ }); export type UploadFileParams = z.infer; + +export interface CheckFileHashResult { + fileType?: string; + isExist: boolean; + metadata?: FileMetadata; + size?: number; + url?: string; +} diff --git a/src/types/meta.ts b/src/types/meta.ts index 459aece85cdb..23ce2b941c2f 100644 --- a/src/types/meta.ts +++ b/src/types/meta.ts @@ -21,19 +21,10 @@ export const LobeMetaDataSchema = z.object({ export type MetaData = z.infer; export interface BaseDataModel { - /** - * @deprecated - */ - createAt?: number; - createdAt: number; id: string; meta: MetaData; - /** - * @deprecated - */ - updateAt?: number; updatedAt: number; } diff --git a/src/types/session/sessionGroup.ts b/src/types/session/sessionGroup.ts index 85fb3675021b..1c8dbcda048a 100644 --- a/src/types/session/sessionGroup.ts +++ b/src/types/session/sessionGroup.ts @@ -8,11 +8,11 @@ export enum SessionDefaultGroup { export type SessionGroupId = SessionDefaultGroup | string; export interface SessionGroupItem { - createdAt: number; + createdAt: Date; id: string; name: string; - sort?: number; - updatedAt: number; + sort?: number | null; + updatedAt: Date; } export type SessionGroups = SessionGroupItem[]; From bc3271aed1e6cc58405a19a9434ee113a8fad433 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 12 Dec 2024 23:18:58 +0800 Subject: [PATCH 02/10] update --- src/database/server/models/file.ts | 24 ++++-------------------- src/database/server/models/user.ts | 2 +- src/services/file/client.ts | 2 +- src/services/user/client.ts | 2 +- src/types/files/upload.ts | 2 +- 5 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/database/server/models/file.ts b/src/database/server/models/file.ts index f14af2a18673..6747dcc68ebe 100644 --- a/src/database/server/models/file.ts +++ b/src/database/server/models/file.ts @@ -64,26 +64,10 @@ export class FileModel { return this.db.insert(globalFiles).values(file).returning(); }; - checkHash = async (hash: string, findInGlobalFiles = true) => { - let item: - | { - fileType: string; - metadata: any; - size: number; - url: string; - } - | undefined; - - if (findInGlobalFiles) { - item = await this.db.query.globalFiles.findFirst({ - where: eq(globalFiles.hashId, hash), - }); - } else { - item = await this.db.query.files.findFirst({ - where: eq(files.fileHash, hash), - }); - } - + checkHash = async (hash: string) => { + const item = await this.db.query.globalFiles.findFirst({ + where: eq(globalFiles.hashId, hash), + }); if (!item) return { isExist: false }; return { diff --git a/src/database/server/models/user.ts b/src/database/server/models/user.ts index 2e20ce27655d..361a064eef14 100644 --- a/src/database/server/models/user.ts +++ b/src/database/server/models/user.ts @@ -94,7 +94,7 @@ export class UserModel { return this.db.delete(userSettings).where(eq(userSettings.id, this.userId)); } - async updateSetting(value: DeepPartial) { + async updateSetting(value: Partial) { const { keyVaults, ...res } = value; // Encrypt keyVaults diff --git a/src/services/file/client.ts b/src/services/file/client.ts index 6d75ae4cb219..44163101c8ec 100644 --- a/src/services/file/client.ts +++ b/src/services/file/client.ts @@ -73,7 +73,7 @@ export class ClientService implements IFileService { } async checkFileHash(hash: string) { - return this.fileModel.checkHash(hash, false); + return this.fileModel.checkHash(hash); } private async getBase64ByFileHash(hash: string) { diff --git a/src/services/user/client.ts b/src/services/user/client.ts index a38c3399173e..6d585404bf2c 100644 --- a/src/services/user/client.ts +++ b/src/services/user/client.ts @@ -43,7 +43,7 @@ export class ClientService implements IUserService { } updateUserSettings = async (patch: DeepPartial) => { - return this.userModel.updateSetting(patch); + return this.userModel.updateSetting(patch as UserSettings); }; resetUserSettings = async () => { diff --git a/src/types/files/upload.ts b/src/types/files/upload.ts index e9d0884ad6d1..b94b69a5fdee 100644 --- a/src/types/files/upload.ts +++ b/src/types/files/upload.ts @@ -91,7 +91,7 @@ export type UploadFileParams = z.infer; export interface CheckFileHashResult { fileType?: string; isExist: boolean; - metadata?: FileMetadata; + metadata?: unknown; size?: number; url?: string; } From 588017369c0b2346be5214b7773d2bf872942ef3 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 12 Dec 2024 23:33:02 +0800 Subject: [PATCH 03/10] fix test --- src/services/file/client.test.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/services/file/client.test.ts b/src/services/file/client.test.ts index b141b0909d9c..9fc2c65f7e80 100644 --- a/src/services/file/client.test.ts +++ b/src/services/file/client.test.ts @@ -1,9 +1,9 @@ import { eq } from 'drizzle-orm'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { Mock, beforeEach, describe, expect, it } from 'vitest'; import { clientDB } from '@/database/client/db'; import { migrate } from '@/database/client/migrate'; -import { files, users } from '@/database/schemas'; +import { files, globalFiles, users } from '@/database/schemas'; import { clientS3Storage } from '@/services/file/ClientS3'; import { UploadFileParams } from '@/types/files'; @@ -66,20 +66,30 @@ describe('FileService', () => { describe('getFile', () => { it('should retrieve and convert local file info to FilePreview', async () => { const fileId = '1'; + const file = { + fileType: 'image/png', + size: 1, + name: 'test.png', + url: 'idb://12312/abc.png', + hashId: '123', + }; + + await clientDB.insert(globalFiles).values(file); + await clientDB.insert(files).values({ id: fileId, userId, - fileType: 'image/png', - size: 1, - name: 'test', + ...file, createdAt: new Date(1), updatedAt: new Date(2), - url: 'idb://12312/abc.png', - fileHash: '1', + fileHash: file.hashId, }); - // (global.URL.createObjectURL as Mock).mockReturnValue('blob:test'); - // (global.Blob as Mock).mockImplementation(() => ['test']); + await clientS3Storage.putObject( + file.hashId, + new File([new ArrayBuffer(1)], file.name, { type: file.fileType }), + ); + const result = await fileService.getFile(fileId); expect(result).toMatchObject({ @@ -87,7 +97,7 @@ describe('FileService', () => { id: '1', size: 1, type: 'image/png', - name: 'test', + name: 'test.png', updatedAt: new Date(2), }); }); From fea9037f6f3c490ca18e219e377507fcbe8c76ae Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 12 Dec 2024 23:55:50 +0800 Subject: [PATCH 04/10] fix test --- src/database/server/models/session.ts | 2 ++ src/store/agent/slices/chat/action.test.ts | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/database/server/models/session.ts b/src/database/server/models/session.ts index ccb145db13bb..494e128e748e 100644 --- a/src/database/server/models/session.ts +++ b/src/database/server/models/session.ts @@ -229,6 +229,8 @@ export class SessionModel { } async updateConfig(id: string, data: Partial) { + if (Object.keys(data).length === 0) return; + return this.db .update(agents) .set(data) diff --git a/src/store/agent/slices/chat/action.test.ts b/src/store/agent/slices/chat/action.test.ts index 5817710b4b3d..cd3472d7c608 100644 --- a/src/store/agent/slices/chat/action.test.ts +++ b/src/store/agent/slices/chat/action.test.ts @@ -1,9 +1,11 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { mutate } from 'swr'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeAll, describe, expect, it, vi } from 'vitest'; import { INBOX_SESSION_ID } from '@/const/session'; -import { DEFAULT_AGENT_CONFIG } from '@/const/settings'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { users } from '@/database/schemas'; import { globalService } from '@/services/global'; import { sessionService } from '@/services/session'; import { useAgentStore } from '@/store/agent'; @@ -19,6 +21,13 @@ vi.mock('swr', async (importOriginal) => { }; }); +beforeAll(async () => { + await migrate(); + await clientDB.transaction(async (trx) => { + await trx.insert(users).values({ id: '123' }); + }); +}); + describe('AgentSlice', () => { describe('removePlugin', () => { it('should call togglePlugin with the provided id and false', async () => { From 4a8e8cfeee0a9a67aa9433870f1a46f650098731 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Fri, 13 Dec 2024 02:27:25 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=9A=A8=20ci:=20fix=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/message/type.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/message/type.ts b/src/services/message/type.ts index 4e197ddd0c6e..4ac08fd54c17 100644 --- a/src/services/message/type.ts +++ b/src/services/message/type.ts @@ -1,4 +1,3 @@ -import { DB_Message } from '@/database/_deprecated/schemas/message'; import { MessageItem } from '@/database/schemas'; import { ChatMessage, From e7b066e023112b281e067aa192f1284218c296ac Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Fri, 13 Dec 2024 13:28:11 +0800 Subject: [PATCH 06/10] Update action.ts --- src/store/user/slices/common/action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/user/slices/common/action.ts b/src/store/user/slices/common/action.ts index 1952e7c6e5e3..0e051dac3a97 100644 --- a/src/store/user/slices/common/action.ts +++ b/src/store/user/slices/common/action.ts @@ -49,7 +49,7 @@ export const createCommonSlice: StateCreator< const { ClientService } = await import('@/services/user/client'); // TODO: add userId - const clientService = new ClientService(''); + const clientService = new ClientService('123'); await clientService.updateAvatar(avatar); await get().refreshUserState(); From dac0a4f1c4cb15065f6949d86faf2491ad3e6d2e Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Fri, 13 Dec 2024 06:33:58 +0000 Subject: [PATCH 07/10] :recycle: refactor: refactor the ClientService userId flow --- src/database/client/db.ts | 44 +++++++++++++++++++++++------ src/services/file/ClientS3/index.ts | 3 ++ src/services/file/client.test.ts | 6 ++-- src/services/file/client.ts | 19 ++++++++++--- src/services/file/index.ts | 4 +-- src/services/import/client.ts | 17 ++++++++--- src/services/import/index.ts | 4 +-- src/services/message/client.ts | 16 ++++++++--- src/services/message/index.ts | 4 +-- src/services/plugin/client.ts | 16 ++++++++--- src/services/plugin/index.ts | 4 +-- src/services/session/client.ts | 22 +++++++++++---- src/services/session/index.ts | 4 +-- src/services/topic/client.ts | 17 ++++++++--- src/services/topic/index.ts | 4 +-- src/services/user/client.ts | 28 +++++++++++------- src/services/user/index.ts | 4 +-- src/types/global.d.ts | 9 ++++++ 18 files changed, 157 insertions(+), 68 deletions(-) diff --git a/src/database/client/db.ts b/src/database/client/db.ts index 5de7ff4f6530..b22b14ad8aaa 100644 --- a/src/database/client/db.ts +++ b/src/database/client/db.ts @@ -1,13 +1,41 @@ -import { IdbFs, PGlite } from '@electric-sql/pglite'; +import { IdbFs, MemoryFS, PGlite } from '@electric-sql/pglite'; import { vector } from '@electric-sql/pglite/vector'; -import { drizzle } from 'drizzle-orm/pglite'; +import { PgliteDatabase, drizzle } from 'drizzle-orm/pglite'; import * as schema from '../schemas'; -const client = new PGlite({ - extensions: { vector }, - fs: new IdbFs('lobechat'), - relaxedDurability: true, -}); +let dbInstance: ReturnType; -export const clientDB = drizzle({ client, schema }); +export function getClientDB() { + // 如果已经初始化过,直接返回实例 + if (dbInstance) return dbInstance; + + // 服务端环境 + if (typeof window === 'undefined') { + const db = new PGlite({ + extensions: { vector }, + fs: new MemoryFS('lobechat'), + }); + return drizzle({ client: db, schema }); + } + + // 客户端环境 + const db = new PGlite({ + extensions: { vector }, + fs: new IdbFs('lobechat'), + relaxedDurability: true, + }); + + dbInstance = drizzle({ client: db, schema }); + return dbInstance; +} + +export const clientDB = getClientDB() as unknown as PgliteDatabase; + +export const getClientDBUserId = () => { + if (typeof window === 'undefined') return undefined; + + return window.__lobeClientUserId; +}; + +export const FALLBACK_CLIENT_DB_USER_ID = 'DEFAULT_LOBE_CHAT_USER'; diff --git a/src/services/file/ClientS3/index.ts b/src/services/file/ClientS3/index.ts index 739f8516866f..e43c318de7f0 100644 --- a/src/services/file/ClientS3/index.ts +++ b/src/services/file/ClientS3/index.ts @@ -6,6 +6,9 @@ class BrowserS3Storage { private store; constructor() { + // skip server-side rendering + if (typeof window === 'undefined') return; + this.store = createStore(BROWSER_S3_DB_NAME, 'objects'); } diff --git a/src/services/file/client.test.ts b/src/services/file/client.test.ts index 9fc2c65f7e80..d4fdc5d1318b 100644 --- a/src/services/file/client.test.ts +++ b/src/services/file/client.test.ts @@ -65,13 +65,13 @@ describe('FileService', () => { describe('getFile', () => { it('should retrieve and convert local file info to FilePreview', async () => { - const fileId = '1'; + const fileId = 'rwlijweled'; const file = { fileType: 'image/png', size: 1, name: 'test.png', url: 'idb://12312/abc.png', - hashId: '123', + hashId: '123tttt', }; await clientDB.insert(globalFiles).values(file); @@ -94,7 +94,7 @@ describe('FileService', () => { expect(result).toMatchObject({ createdAt: new Date(1), - id: '1', + id: 'rwlijweled', size: 1, type: 'image/png', name: 'test.png', diff --git a/src/services/file/client.ts b/src/services/file/client.ts index 44163101c8ec..b3d1cd8e2cac 100644 --- a/src/services/file/client.ts +++ b/src/services/file/client.ts @@ -1,4 +1,4 @@ -import { clientDB } from '@/database/client/db'; +import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; import { FileModel } from '@/database/server/models/file'; import { clientS3Storage } from '@/services/file/ClientS3'; import { FileItem, UploadFileParams } from '@/types/files'; @@ -6,10 +6,21 @@ import { FileItem, UploadFileParams } from '@/types/files'; import { IFileService } from './type'; export class ClientService implements IFileService { - private fileModel: FileModel; + private readonly fallbackUserId: string; - constructor(userId: string) { - this.fileModel = new FileModel(clientDB as any, userId); + private get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + private get fileModel(): FileModel { + console.time('new FileModel'); + const model = new FileModel(clientDB as any, this.userId); + console.timeEnd('new FileModel'); + return model; + } + + constructor(userId?: string) { + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; } async createFile(file: UploadFileParams) { diff --git a/src/services/file/index.ts b/src/services/file/index.ts index 18bdfba27045..72bb54f91872 100644 --- a/src/services/file/index.ts +++ b/src/services/file/index.ts @@ -2,6 +2,4 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const fileService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' - ? new ServerService() - : new ClientService('123'); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); diff --git a/src/services/import/client.ts b/src/services/import/client.ts index d18a09507248..72738554bb4e 100644 --- a/src/services/import/client.ts +++ b/src/services/import/client.ts @@ -1,13 +1,22 @@ -import { clientDB } from '@/database/client/db'; +import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; import { DataImporterRepos } from '@/database/repositories/dataImporter'; import { useUserStore } from '@/store/user'; import { ImportStage, ImporterEntryData, OnImportCallbacks } from '@/types/importer'; import { UserSettings } from '@/types/user/settings'; export class ClientService { - private dataImporter: DataImporterRepos; - constructor(userId: string) { - this.dataImporter = new DataImporterRepos(clientDB as any, userId); + private readonly fallbackUserId: string; + + private get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + private get dataImporter(): DataImporterRepos { + return new DataImporterRepos(clientDB as any, this.userId); + } + + constructor(userId?: string) { + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; } importSettings = async (settings: UserSettings) => { diff --git a/src/services/import/index.ts b/src/services/import/index.ts index a94e4e6d1de6..4f2ab75bfea8 100644 --- a/src/services/import/index.ts +++ b/src/services/import/index.ts @@ -2,6 +2,4 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const importService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' - ? new ServerService() - : new ClientService('123'); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); diff --git a/src/services/message/client.ts b/src/services/message/client.ts index 56d3e59fd57b..a1b5d9c8cdf2 100644 --- a/src/services/message/client.ts +++ b/src/services/message/client.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs'; -import { clientDB } from '@/database/client/db'; +import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; import { MessageItem } from '@/database/schemas'; import { MessageModel } from '@/database/server/models/message'; import { @@ -14,10 +14,18 @@ import { import { IMessageService } from './type'; export class ClientService implements IMessageService { - private messageModel: MessageModel; + private readonly fallbackUserId: string; - constructor(userId: string) { - this.messageModel = new MessageModel(clientDB as any, userId); + private get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + private get messageModel(): MessageModel { + return new MessageModel(clientDB as any, this.userId); + } + + constructor(userId?: string) { + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; } async createMessage(data: CreateMessageParams) { diff --git a/src/services/message/index.ts b/src/services/message/index.ts index c1c1fbdb542b..930eaf6034ed 100644 --- a/src/services/message/index.ts +++ b/src/services/message/index.ts @@ -2,6 +2,4 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const messageService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' - ? new ServerService() - : new ClientService('123'); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); diff --git a/src/services/plugin/client.ts b/src/services/plugin/client.ts index 1e534c5500e8..84629b99e1f4 100644 --- a/src/services/plugin/client.ts +++ b/src/services/plugin/client.ts @@ -1,6 +1,6 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { clientDB } from '@/database/client/db'; +import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; import { PluginModel } from '@/database/server/models/plugin'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; @@ -8,10 +8,18 @@ import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { IPluginService, InstallPluginParams } from './type'; export class ClientService implements IPluginService { - private pluginModel: PluginModel; + private readonly fallbackUserId: string; - constructor(userId: string) { - this.pluginModel = new PluginModel(clientDB as any, userId); + private get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + private get pluginModel(): PluginModel { + return new PluginModel(clientDB as any, this.userId); + } + + constructor(userId?: string) { + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; } installPlugin = async (plugin: InstallPluginParams) => { diff --git a/src/services/plugin/index.ts b/src/services/plugin/index.ts index 58265d316138..77b3ab38869b 100644 --- a/src/services/plugin/index.ts +++ b/src/services/plugin/index.ts @@ -2,6 +2,4 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const pluginService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' - ? new ServerService() - : new ClientService('123'); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); diff --git a/src/services/session/client.ts b/src/services/session/client.ts index 3592bc1f4691..1635b3f577ea 100644 --- a/src/services/session/client.ts +++ b/src/services/session/client.ts @@ -2,7 +2,7 @@ import { DeepPartial } from 'utility-types'; import { INBOX_SESSION_ID } from '@/const/session'; import { UserModel } from '@/database/_deprecated/models/user'; -import { clientDB } from '@/database/client/db'; +import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; import { AgentItem } from '@/database/schemas'; import { SessionModel } from '@/database/server/models/session'; import { SessionGroupModel } from '@/database/server/models/sessionGroup'; @@ -22,12 +22,22 @@ import { merge } from '@/utils/merge'; import { ISessionService } from './type'; export class ClientService implements ISessionService { - private sessionModel: SessionModel; - private sessionGroupModel: SessionGroupModel; + private readonly fallbackUserId: string; - constructor(userId: string) { - this.sessionGroupModel = new SessionGroupModel(clientDB as any, userId); - this.sessionModel = new SessionModel(clientDB as any, userId); + private get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + private get sessionModel(): SessionModel { + return new SessionModel(clientDB as any, this.userId); + } + + private get sessionGroupModel(): SessionGroupModel { + return new SessionGroupModel(clientDB as any, this.userId); + } + + constructor(userId?: string) { + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; } async createSession(type: LobeSessionType, data: Partial): Promise { diff --git a/src/services/session/index.ts b/src/services/session/index.ts index 34f8d766a500..0d55e3cc45e5 100644 --- a/src/services/session/index.ts +++ b/src/services/session/index.ts @@ -2,6 +2,4 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const sessionService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' - ? new ServerService() - : new ClientService('123'); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); diff --git a/src/services/topic/client.ts b/src/services/topic/client.ts index 337b8bcb2bba..2b950976204e 100644 --- a/src/services/topic/client.ts +++ b/src/services/topic/client.ts @@ -1,13 +1,22 @@ -import { clientDB } from '@/database/client/db'; +import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; import { TopicModel } from '@/database/server/models/topic'; import { ChatTopic } from '@/types/topic'; import { CreateTopicParams, ITopicService, QueryTopicParams } from './type'; export class ClientService implements ITopicService { - private topicModel: TopicModel; - constructor(userId: string) { - this.topicModel = new TopicModel(clientDB as any, userId); + private readonly fallbackUserId: string; + + private get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + private get topicModel(): TopicModel { + return new TopicModel(clientDB as any, this.userId); + } + + constructor(userId?: string) { + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; } async createTopic(params: CreateTopicParams): Promise { diff --git a/src/services/topic/index.ts b/src/services/topic/index.ts index 1c9330e9a499..7105ac9765dc 100644 --- a/src/services/topic/index.ts +++ b/src/services/topic/index.ts @@ -2,6 +2,4 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const topicService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' - ? new ServerService() - : new ClientService('123'); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); diff --git a/src/services/user/client.ts b/src/services/user/client.ts index 6d585404bf2c..7c34f92cbafe 100644 --- a/src/services/user/client.ts +++ b/src/services/user/client.ts @@ -1,6 +1,6 @@ import { DeepPartial } from 'utility-types'; -import { clientDB } from '@/database/client/db'; +import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; import { MessageModel } from '@/database/server/models/message'; import { SessionModel } from '@/database/server/models/session'; import { UserModel } from '@/database/server/models/user'; @@ -12,17 +12,25 @@ import { IUserService } from './type'; export class ClientService implements IUserService { private preferenceStorage: AsyncLocalStorage; - private userModel: UserModel; - private messageModel: MessageModel; - private sessionModel: SessionModel; - private userId: string; + private readonly fallbackUserId: string; - constructor(userId: string) { + private get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + private get userModel(): UserModel { + return new UserModel(clientDB as any, this.userId); + } + private get messageModel(): MessageModel { + return new MessageModel(clientDB as any, this.userId); + } + private get sessionModel(): SessionModel { + return new SessionModel(clientDB as any, this.userId); + } + + constructor(userId?: string) { this.preferenceStorage = new AsyncLocalStorage('LOBE_PREFERENCE'); - this.userModel = new UserModel(clientDB as any, userId); - this.userId = userId; - this.messageModel = new MessageModel(clientDB as any, userId); - this.sessionModel = new SessionModel(clientDB as any, userId); + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; } async getUserState(): Promise { diff --git a/src/services/user/index.ts b/src/services/user/index.ts index 80272a68900a..e472bdaef2c0 100644 --- a/src/services/user/index.ts +++ b/src/services/user/index.ts @@ -2,6 +2,4 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const userService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' - ? new ServerService() - : new ClientService('123'); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 0d4762a75d2a..074ec166c3a0 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -12,3 +12,12 @@ declare module 'antd-style' { declare module 'styled-components' { export interface DefaultTheme extends AntdToken, LobeCustomToken {} } + +declare global { + interface Window { + /** + * 客户端 db 模式下的用户 id + */ + __lobeClientUserId?: string; + } +} From 350b4734c0d8bdd37f1273d7b5637574437db162 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Fri, 13 Dec 2024 06:50:08 +0000 Subject: [PATCH 08/10] refactor with BaseClientService --- src/services/baseClientService/index.ts | 13 +++++++++++++ src/services/file/client.ts | 20 ++++---------------- src/services/import/client.ts | 15 +++------------ src/services/message/client.ts | 15 +++------------ src/services/plugin/client.ts | 15 +++------------ src/services/session/client.ts | 15 +++------------ src/services/topic/client.ts | 15 +++------------ src/services/user/client.ts | 12 ++++-------- 8 files changed, 36 insertions(+), 84 deletions(-) create mode 100644 src/services/baseClientService/index.ts diff --git a/src/services/baseClientService/index.ts b/src/services/baseClientService/index.ts new file mode 100644 index 000000000000..60084cfa44fa --- /dev/null +++ b/src/services/baseClientService/index.ts @@ -0,0 +1,13 @@ +import { FALLBACK_CLIENT_DB_USER_ID, getClientDBUserId } from '@/database/client/db'; + +export class BaseClientService { + private readonly fallbackUserId: string; + + protected get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + constructor(userId?: string) { + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; + } +} diff --git a/src/services/file/client.ts b/src/services/file/client.ts index b3d1cd8e2cac..b6c2818b6c39 100644 --- a/src/services/file/client.ts +++ b/src/services/file/client.ts @@ -1,26 +1,14 @@ -import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; +import { clientDB } from '@/database/client/db'; import { FileModel } from '@/database/server/models/file'; +import { BaseClientService } from '@/services/baseClientService'; import { clientS3Storage } from '@/services/file/ClientS3'; import { FileItem, UploadFileParams } from '@/types/files'; import { IFileService } from './type'; -export class ClientService implements IFileService { - private readonly fallbackUserId: string; - - private get userId(): string { - return getClientDBUserId() || this.fallbackUserId; - } - +export class ClientService extends BaseClientService implements IFileService { private get fileModel(): FileModel { - console.time('new FileModel'); - const model = new FileModel(clientDB as any, this.userId); - console.timeEnd('new FileModel'); - return model; - } - - constructor(userId?: string) { - this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; + return new FileModel(clientDB as any, this.userId); } async createFile(file: UploadFileParams) { diff --git a/src/services/import/client.ts b/src/services/import/client.ts index 72738554bb4e..0dceca6f8fef 100644 --- a/src/services/import/client.ts +++ b/src/services/import/client.ts @@ -1,24 +1,15 @@ -import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; +import { clientDB } from '@/database/client/db'; import { DataImporterRepos } from '@/database/repositories/dataImporter'; +import { BaseClientService } from '@/services/baseClientService'; import { useUserStore } from '@/store/user'; import { ImportStage, ImporterEntryData, OnImportCallbacks } from '@/types/importer'; import { UserSettings } from '@/types/user/settings'; -export class ClientService { - private readonly fallbackUserId: string; - - private get userId(): string { - return getClientDBUserId() || this.fallbackUserId; - } - +export class ClientService extends BaseClientService { private get dataImporter(): DataImporterRepos { return new DataImporterRepos(clientDB as any, this.userId); } - constructor(userId?: string) { - this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; - } - importSettings = async (settings: UserSettings) => { await useUserStore.getState().importAppSettings(settings); }; diff --git a/src/services/message/client.ts b/src/services/message/client.ts index a1b5d9c8cdf2..f0ed468a7e67 100644 --- a/src/services/message/client.ts +++ b/src/services/message/client.ts @@ -1,8 +1,9 @@ import dayjs from 'dayjs'; -import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; +import { clientDB } from '@/database/client/db'; import { MessageItem } from '@/database/schemas'; import { MessageModel } from '@/database/server/models/message'; +import { BaseClientService } from '@/services/baseClientService'; import { ChatMessage, ChatMessageError, @@ -13,21 +14,11 @@ import { import { IMessageService } from './type'; -export class ClientService implements IMessageService { - private readonly fallbackUserId: string; - - private get userId(): string { - return getClientDBUserId() || this.fallbackUserId; - } - +export class ClientService extends BaseClientService implements IMessageService { private get messageModel(): MessageModel { return new MessageModel(clientDB as any, this.userId); } - constructor(userId?: string) { - this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; - } - async createMessage(data: CreateMessageParams) { const { id } = await this.messageModel.create(data); diff --git a/src/services/plugin/client.ts b/src/services/plugin/client.ts index 84629b99e1f4..b8cb130566aa 100644 --- a/src/services/plugin/client.ts +++ b/src/services/plugin/client.ts @@ -1,27 +1,18 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; +import { clientDB } from '@/database/client/db'; import { PluginModel } from '@/database/server/models/plugin'; +import { BaseClientService } from '@/services/baseClientService'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { IPluginService, InstallPluginParams } from './type'; -export class ClientService implements IPluginService { - private readonly fallbackUserId: string; - - private get userId(): string { - return getClientDBUserId() || this.fallbackUserId; - } - +export class ClientService extends BaseClientService implements IPluginService { private get pluginModel(): PluginModel { return new PluginModel(clientDB as any, this.userId); } - constructor(userId?: string) { - this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; - } - installPlugin = async (plugin: InstallPluginParams) => { await this.pluginModel.create(plugin); return; diff --git a/src/services/session/client.ts b/src/services/session/client.ts index 1635b3f577ea..2d15f9b59ad1 100644 --- a/src/services/session/client.ts +++ b/src/services/session/client.ts @@ -2,10 +2,11 @@ import { DeepPartial } from 'utility-types'; import { INBOX_SESSION_ID } from '@/const/session'; import { UserModel } from '@/database/_deprecated/models/user'; -import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; +import { clientDB } from '@/database/client/db'; import { AgentItem } from '@/database/schemas'; import { SessionModel } from '@/database/server/models/session'; import { SessionGroupModel } from '@/database/server/models/sessionGroup'; +import { BaseClientService } from '@/services/baseClientService'; import { useUserStore } from '@/store/user'; import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent'; import { MetaData } from '@/types/meta'; @@ -21,13 +22,7 @@ import { merge } from '@/utils/merge'; import { ISessionService } from './type'; -export class ClientService implements ISessionService { - private readonly fallbackUserId: string; - - private get userId(): string { - return getClientDBUserId() || this.fallbackUserId; - } - +export class ClientService extends BaseClientService implements ISessionService { private get sessionModel(): SessionModel { return new SessionModel(clientDB as any, this.userId); } @@ -36,10 +31,6 @@ export class ClientService implements ISessionService { return new SessionGroupModel(clientDB as any, this.userId); } - constructor(userId?: string) { - this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; - } - async createSession(type: LobeSessionType, data: Partial): Promise { const { config, group, meta, ...session } = data; diff --git a/src/services/topic/client.ts b/src/services/topic/client.ts index 2b950976204e..08ae914bd8b9 100644 --- a/src/services/topic/client.ts +++ b/src/services/topic/client.ts @@ -1,24 +1,15 @@ -import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; +import { clientDB } from '@/database/client/db'; import { TopicModel } from '@/database/server/models/topic'; +import { BaseClientService } from '@/services/baseClientService'; import { ChatTopic } from '@/types/topic'; import { CreateTopicParams, ITopicService, QueryTopicParams } from './type'; -export class ClientService implements ITopicService { - private readonly fallbackUserId: string; - - private get userId(): string { - return getClientDBUserId() || this.fallbackUserId; - } - +export class ClientService extends BaseClientService implements ITopicService { private get topicModel(): TopicModel { return new TopicModel(clientDB as any, this.userId); } - constructor(userId?: string) { - this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; - } - async createTopic(params: CreateTopicParams): Promise { const item = await this.topicModel.create(params as any); diff --git a/src/services/user/client.ts b/src/services/user/client.ts index 7c34f92cbafe..3bc8b5b9b2fa 100644 --- a/src/services/user/client.ts +++ b/src/services/user/client.ts @@ -1,22 +1,18 @@ import { DeepPartial } from 'utility-types'; -import { FALLBACK_CLIENT_DB_USER_ID, clientDB, getClientDBUserId } from '@/database/client/db'; +import { clientDB } from '@/database/client/db'; import { MessageModel } from '@/database/server/models/message'; import { SessionModel } from '@/database/server/models/session'; import { UserModel } from '@/database/server/models/user'; +import { BaseClientService } from '@/services/baseClientService'; import { UserGuide, UserInitializationState, UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; import { AsyncLocalStorage } from '@/utils/localStorage'; import { IUserService } from './type'; -export class ClientService implements IUserService { +export class ClientService extends BaseClientService implements IUserService { private preferenceStorage: AsyncLocalStorage; - private readonly fallbackUserId: string; - - private get userId(): string { - return getClientDBUserId() || this.fallbackUserId; - } private get userModel(): UserModel { return new UserModel(clientDB as any, this.userId); @@ -29,8 +25,8 @@ export class ClientService implements IUserService { } constructor(userId?: string) { + super(userId); this.preferenceStorage = new AsyncLocalStorage('LOBE_PREFERENCE'); - this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; } async getUserState(): Promise { From e15362b3ac531e87d534815103d402f3a00ca28c Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Fri, 13 Dec 2024 06:51:52 +0000 Subject: [PATCH 09/10] clean code --- src/database/client/db.ts | 8 -------- src/services/baseClientService/index.ts | 8 +++++++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/database/client/db.ts b/src/database/client/db.ts index b22b14ad8aaa..d79f688dc63e 100644 --- a/src/database/client/db.ts +++ b/src/database/client/db.ts @@ -31,11 +31,3 @@ export function getClientDB() { } export const clientDB = getClientDB() as unknown as PgliteDatabase; - -export const getClientDBUserId = () => { - if (typeof window === 'undefined') return undefined; - - return window.__lobeClientUserId; -}; - -export const FALLBACK_CLIENT_DB_USER_ID = 'DEFAULT_LOBE_CHAT_USER'; diff --git a/src/services/baseClientService/index.ts b/src/services/baseClientService/index.ts index 60084cfa44fa..2717d974c783 100644 --- a/src/services/baseClientService/index.ts +++ b/src/services/baseClientService/index.ts @@ -1,4 +1,10 @@ -import { FALLBACK_CLIENT_DB_USER_ID, getClientDBUserId } from '@/database/client/db'; +const getClientDBUserId = () => { + if (typeof window === 'undefined') return undefined; + + return window.__lobeClientUserId; +}; + +const FALLBACK_CLIENT_DB_USER_ID = 'DEFAULT_LOBE_CHAT_USER'; export class BaseClientService { private readonly fallbackUserId: string; From 282d7d4855b05cfb39f9770cab06b478be3b4c7e Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Fri, 13 Dec 2024 07:21:22 +0000 Subject: [PATCH 10/10] fix init --- src/services/user/client.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/services/user/client.ts b/src/services/user/client.ts index 3bc8b5b9b2fa..c1173c6f6acd 100644 --- a/src/services/user/client.ts +++ b/src/services/user/client.ts @@ -1,6 +1,7 @@ import { DeepPartial } from 'utility-types'; import { clientDB } from '@/database/client/db'; +import { UserItem, users } from '@/database/schemas'; import { MessageModel } from '@/database/server/models/message'; import { SessionModel } from '@/database/server/models/session'; import { UserModel } from '@/database/server/models/user'; @@ -8,6 +9,7 @@ import { BaseClientService } from '@/services/baseClientService'; import { UserGuide, UserInitializationState, UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; import { AsyncLocalStorage } from '@/utils/localStorage'; +import { uuid } from '@/utils/uuid'; import { IUserService } from './type'; @@ -30,6 +32,10 @@ export class ClientService extends BaseClientService implements IUserService { } async getUserState(): Promise { + // if user not exist in the db, create one to make sure the user exist + // and init the window.__lobeClientUserId + await this.makeSureUserExist(); + const state = await this.userModel.getUserState(); const user = await UserModel.findById(clientDB as any, this.userId); const messageCount = await this.messageModel.count(); @@ -66,4 +72,19 @@ export class ClientService extends BaseClientService implements IUserService { async updateGuide(guide: Partial) { throw new Error('Method not implemented.'); } + + private async makeSureUserExist() { + const existUsers = await clientDB.query.users.findMany(); + + let user: { id: string }; + if (existUsers.length === 0) { + user = await clientDB.insert(users).values({ id: uuid() }).returning(); + } else { + user = existUsers[0]; + } + + if (typeof window !== 'undefined') { + window.__lobeClientUserId = user.id; + } + } }