From 0f889952dd3e63599896d426836b315458b0825b Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Fri, 2 Jan 2026 20:12:19 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20model=20edit=20icon?= =?UTF-8?q?=20missing=20(#11105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: fix model edit icon missing * fix stats welcome * refactor pglite db case * fix e2e tests * update docs --- e2e/docs/testing-tips.md | 30 ++ e2e/src/steps/discover/smoke.steps.ts | 44 +-- locales/zh-CN/auth.json | 2 +- package.json | 1 - packages/database/package.json | 2 +- packages/database/src/client/db.test.ts | 161 ++--------- packages/database/src/client/db.ts | 260 ++---------------- packages/database/src/client/pglite.ts | 17 -- packages/database/src/client/pglite.worker.ts | 25 -- .../database/src/models/__tests__/_util.ts | 22 +- .../provider/features/ModelList/ModelItem.tsx | 15 +- src/libs/next/config/define-config.ts | 2 +- 12 files changed, 115 insertions(+), 466 deletions(-) delete mode 100644 packages/database/src/client/pglite.ts delete mode 100644 packages/database/src/client/pglite.worker.ts diff --git a/e2e/docs/testing-tips.md b/e2e/docs/testing-tips.md index 596c4f7442..7e1de44cee 100644 --- a/e2e/docs/testing-tips.md +++ b/e2e/docs/testing-tips.md @@ -64,6 +64,36 @@ HEADLESS=false pnpm exec cucumber-js --config cucumber.config.js --tags "@smoke" ## 常见问题 +### waitForLoadState ('networkidle') 超时 + +**原因**: `networkidle` 表示 500ms 内没有网络请求。在 CI 环境中,由于分析脚本、外部资源加载、轮询等持续网络活动,这个状态可能永远无法达到。 + +**错误示例**: + +``` +page.waitForLoadState: Timeout 10000ms exceeded. +=========================== logs =========================== + "load" event fired +============================================================ +``` + +**解决**: + +- **避免使用 `networkidle`** - 这是不可靠的等待策略 +- **直接等待目标元素** - 使用 `expect(element).toBeVisible({ timeout: 30_000 })` 替代 +- 如果必须等待页面加载完成,使用 `domcontentloaded` 或 `load` 事件 + +```typescript +// ❌ 不推荐 - networkidle 在 CI 中容易超时 +await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); +const element = this.page.locator('[data-testid="my-element"]'); +await expect(element).toBeVisible(); + +// ✅ 推荐 - 直接等待目标元素 +const element = this.page.locator('[data-testid="my-element"]'); +await expect(element).toBeVisible({ timeout: 30_000 }); +``` + ### 测试超时 (function timed out) **原因**: 元素定位失败或等待时间不足 diff --git a/e2e/src/steps/discover/smoke.steps.ts b/e2e/src/steps/discover/smoke.steps.ts index f57441879f..0f2208eabf 100644 --- a/e2e/src/steps/discover/smoke.steps.ts +++ b/e2e/src/steps/discover/smoke.steps.ts @@ -9,50 +9,40 @@ import { CustomWorld } from '../../support/world'; // Home Page Steps Then('I should see the featured assistants section', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // Look for "Featured Agents" heading text (i18n key: home.featuredAssistants) // Supports: en-US "Featured Agents", zh-CN "推荐助理" const featuredSection = this.page .getByRole('heading', { name: /featured agents|推荐助理/i }) .first(); - await expect(featuredSection).toBeVisible({ timeout: 10_000 }); + await expect(featuredSection).toBeVisible({ timeout: 30_000 }); }); Then('I should see the featured MCP tools section', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // Look for "Featured Skills" heading text (i18n key: home.featuredTools) // Supports: en-US "Featured Skills", zh-CN "推荐技能" const mcpSection = this.page.getByRole('heading', { name: /featured skills|推荐技能/i }).first(); - await expect(mcpSection).toBeVisible({ timeout: 10_000 }); + await expect(mcpSection).toBeVisible({ timeout: 30_000 }); }); // Assistant List Page Steps Then('I should see the search bar', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // SearchBar component has data-testid="search-bar" const searchBar = this.page.locator('[data-testid="search-bar"]').first(); - await expect(searchBar).toBeVisible({ timeout: 10_000 }); + await expect(searchBar).toBeVisible({ timeout: 30_000 }); }); Then('I should see the category menu', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // CategoryMenu component has data-testid="category-menu" const categoryMenu = this.page.locator('[data-testid="category-menu"]').first(); - await expect(categoryMenu).toBeVisible({ timeout: 10_000 }); + await expect(categoryMenu).toBeVisible({ timeout: 30_000 }); }); Then('I should see assistant cards', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // Look for assistant items by data-testid const assistantItems = this.page.locator('[data-testid="assistant-item"]'); // Wait for at least one item to be visible - await expect(assistantItems.first()).toBeVisible({ timeout: 10_000 }); + await expect(assistantItems.first()).toBeVisible({ timeout: 30_000 }); // Check we have multiple items const count = await assistantItems.count(); @@ -60,22 +50,18 @@ Then('I should see assistant cards', async function (this: CustomWorld) { }); Then('I should see pagination controls', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // Pagination component has data-testid="pagination" const pagination = this.page.locator('[data-testid="pagination"]').first(); - await expect(pagination).toBeVisible({ timeout: 10_000 }); + await expect(pagination).toBeVisible({ timeout: 30_000 }); }); // Model List Page Steps Then('I should see model cards', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // Model items have data-testid="model-item" const modelItems = this.page.locator('[data-testid="model-item"]'); // Wait for at least one item to be visible - await expect(modelItems.first()).toBeVisible({ timeout: 10_000 }); + await expect(modelItems.first()).toBeVisible({ timeout: 30_000 }); // Check we have multiple items const count = await modelItems.count(); @@ -83,22 +69,18 @@ Then('I should see model cards', async function (this: CustomWorld) { }); Then('I should see the sort dropdown', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // SortButton has data-testid="sort-dropdown" const sortDropdown = this.page.locator('[data-testid="sort-dropdown"]').first(); - await expect(sortDropdown).toBeVisible({ timeout: 10_000 }); + await expect(sortDropdown).toBeVisible({ timeout: 30_000 }); }); // Provider List Page Steps Then('I should see provider cards', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // Look for provider items by data-testid const providerItems = this.page.locator('[data-testid="provider-item"]'); // Wait for at least one item to be visible - await expect(providerItems.first()).toBeVisible({ timeout: 10_000 }); + await expect(providerItems.first()).toBeVisible({ timeout: 30_000 }); // Check we have multiple items const count = await providerItems.count(); @@ -107,13 +89,11 @@ Then('I should see provider cards', async function (this: CustomWorld) { // MCP List Page Steps Then('I should see MCP cards', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // Look for MCP items by data-testid const mcpItems = this.page.locator('[data-testid="mcp-item"]'); // Wait for at least one item to be visible - await expect(mcpItems.first()).toBeVisible({ timeout: 10_000 }); + await expect(mcpItems.first()).toBeVisible({ timeout: 30_000 }); // Check we have multiple items const count = await mcpItems.count(); @@ -121,9 +101,7 @@ Then('I should see MCP cards', async function (this: CustomWorld) { }); Then('I should see the category filter', async function (this: CustomWorld) { - await this.page.waitForLoadState('networkidle', { timeout: 10_000 }); - // CategoryMenu component has data-testid="category-menu" (shared across list pages) const categoryFilter = this.page.locator('[data-testid="category-menu"]').first(); - await expect(categoryFilter).toBeVisible({ timeout: 10_000 }); + await expect(categoryFilter).toBeVisible({ timeout: 30_000 }); }); diff --git a/locales/zh-CN/auth.json b/locales/zh-CN/auth.json index bbb6042181..debf7c694a 100644 --- a/locales/zh-CN/auth.json +++ b/locales/zh-CN/auth.json @@ -214,7 +214,7 @@ "stats.topicsRank.right": "消息数", "stats.topicsRank.title": "话题内容量", "stats.updatedAt": "更新至", - "stats.welcome": "{{name}},这是你与 {{appName}} 一起记录协作的第 {{days}} 天", + "stats.welcome": "{{username}},这是你与 {{appName}} 一起记录协作的第 {{days}} 天", "stats.words": "累计字数", "tab.apikey": "API Key", "tab.profile": "账号", diff --git a/package.json b/package.json index 65fd695aed..69fe389969 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,6 @@ "@codesandbox/sandpack-react": "^2.20.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/utilities": "^3.2.2", - "@electric-sql/pglite": "0.2.17", "@emotion/react": "^11.14.0", "@fal-ai/client": "^1.8.0", "@formkit/auto-animate": "^0.9.0", diff --git a/packages/database/package.json b/packages/database/package.json index ca302bacd4..4b912268a9 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -23,11 +23,11 @@ "ws": "^8.18.3" }, "devDependencies": { + "@electric-sql/pglite": "^0.3.14", "dotenv": "^17.2.3", "fake-indexeddb": "^6.2.5" }, "peerDependencies": { - "@electric-sql/pglite": "^0.2.17", "dayjs": ">=1.11.19", "drizzle-orm": ">=0.44.7", "nanoid": ">=5.1.6", diff --git a/packages/database/src/client/db.test.ts b/packages/database/src/client/db.test.ts index 4fef2a31cb..d3cbc786ba 100644 --- a/packages/database/src/client/db.test.ts +++ b/packages/database/src/client/db.test.ts @@ -1,18 +1,11 @@ -import { ClientDBLoadingProgress, DatabaseLoadingState } from '@lobechat/types'; +import { PGlite } from '@electric-sql/pglite'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DatabaseManager } from './db'; - -// Mock 所有外部依赖 vi.mock('@electric-sql/pglite', () => ({ - default: vi.fn(), - IdbFs: vi.fn(), - PGlite: vi.fn(), - MemoryFS: vi.fn(), + PGlite: vi.fn(() => ({})), })); vi.mock('@electric-sql/pglite/vector', () => ({ - default: vi.fn(), vector: vi.fn(), })); @@ -24,154 +17,36 @@ vi.mock('drizzle-orm/pglite', () => ({ })), })); -let manager: DatabaseManager; -let progressEvents: ClientDBLoadingProgress[] = []; -let stateChanges: DatabaseLoadingState[] = []; - -let callbacks = { - onProgress: vi.fn((progress: ClientDBLoadingProgress) => { - progressEvents.push(progress); - }), - onStateChange: vi.fn((state: DatabaseLoadingState) => { - stateChanges.push(state); - }), -}; - beforeEach(() => { vi.clearAllMocks(); - progressEvents = []; - stateChanges = []; - - callbacks = { - onProgress: vi.fn((progress: ClientDBLoadingProgress) => { - progressEvents.push(progress); - }), - onStateChange: vi.fn((state: DatabaseLoadingState) => { - stateChanges.push(state); - }), - }; - // @ts-expect-error - DatabaseManager['instance'] = undefined; - manager = DatabaseManager.getInstance(); + vi.resetModules(); }); describe('DatabaseManager', () => { - describe('Callback Handling', () => { - it( - 'should properly track loading states', - async () => { - await manager.initialize(callbacks); + describe('initializeDB', () => { + it('should initialize database with PGlite', async () => { + const { initializeDB } = await import('./db'); + await initializeDB(); - // 验证状态转换顺序 - expect(stateChanges).toEqual([ - DatabaseLoadingState.Initializing, - DatabaseLoadingState.LoadingDependencies, - DatabaseLoadingState.LoadingWasm, - DatabaseLoadingState.Migrating, - DatabaseLoadingState.Finished, - DatabaseLoadingState.Ready, - ]); - }, - { - timeout: 15000, - }, - ); - - it('should report dependencies loading progress', async () => { - await manager.initialize(callbacks); - - // 验证依赖加载进度回调 - const dependencyProgress = progressEvents.filter((e) => e.phase === 'dependencies'); - expect(dependencyProgress.length).toBeGreaterThan(0); - expect(dependencyProgress[dependencyProgress.length - 1]).toEqual( - expect.objectContaining({ - phase: 'dependencies', - progress: 100, - costTime: expect.any(Number), - }), - ); - }); - - it('should report WASM loading progress', async () => { - await manager.initialize(callbacks); - - // 验证 WASM 加载进度回调 - const wasmProgress = progressEvents.filter((e) => e.phase === 'wasm'); - // expect(wasmProgress.length).toBeGreaterThan(0); - expect(wasmProgress[wasmProgress.length - 1]).toEqual( - expect.objectContaining({ - phase: 'wasm', - progress: 100, - costTime: expect.any(Number), - }), - ); - }); - - it('should handle initialization errors', async () => { - // 模拟加载失败 - vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error')); - - await expect(manager.initialize(callbacks)).rejects.toThrow(); - expect(stateChanges).toContain(DatabaseLoadingState.Error); - }); - - it('should only initialize once when called multiple times', async () => { - const firstInit = manager.initialize(callbacks); - const secondInit = manager.initialize(callbacks); - - await Promise.all([firstInit, secondInit]); - - // 验证回调只被调用一次 - const readyStateCount = stateChanges.filter( - (state) => state === DatabaseLoadingState.Ready, - ).length; - expect(readyStateCount).toBe(1); - }); - }); - - describe('Progress Calculation', () => { - it('should report progress between 0 and 100', async () => { - await manager.initialize(callbacks); - - // 验证所有进度值都在有效范围内 - progressEvents.forEach((event) => { - expect(event.progress).toBeGreaterThanOrEqual(0); - expect(event.progress).toBeLessThanOrEqual(100); + expect(PGlite).toHaveBeenCalledWith('idb://lobechat', { + extensions: { vector: expect.any(Function) }, + relaxedDurability: true, }); }); - it('should include timing information', async () => { - await manager.initialize(callbacks); + it('should only initialize once when called multiple times', async () => { + const { initializeDB } = await import('./db'); + await Promise.all([initializeDB(), initializeDB()]); - // 验证最终进度回调包含耗时信息 - const finalProgress = progressEvents[progressEvents.length - 1]; - expect(finalProgress.costTime).toBeGreaterThan(0); + expect(PGlite).toHaveBeenCalledTimes(1); }); }); - describe('Error Handling', () => { - it('should handle missing callbacks gracefully', async () => { - // 测试没有提供回调的情况 - await expect(manager.initialize()).resolves.toBeDefined(); - }); - - it('should handle partial callbacks', async () => { - // 只提供部分回调 - await expect(manager.initialize({ onProgress: callbacks.onProgress })).resolves.toBeDefined(); - await expect( - manager.initialize({ onStateChange: callbacks.onStateChange }), - ).resolves.toBeDefined(); - }); - }); - - describe('Database Access', () => { - it('should throw error when accessing database before initialization', () => { - expect(() => manager.db).toThrow('Database not initialized'); - }); - + describe('clientDB proxy', () => { it('should provide access to database after initialization', async () => { - await manager.initialize(); - expect(() => manager.db).not.toThrow(); + const { clientDB, initializeDB } = await import('./db'); + await initializeDB(); + expect(clientDB).toBeDefined(); }); }); }); diff --git a/packages/database/src/client/db.ts b/packages/database/src/client/db.ts index ef9a67c9bd..499138d25a 100644 --- a/packages/database/src/client/db.ts +++ b/packages/database/src/client/db.ts @@ -1,53 +1,24 @@ -import { - type ClientDBLoadingProgress, - DatabaseLoadingState, - type MigrationSQL, - type MigrationTableItem, -} from '@lobechat/types'; +import { PGlite } from '@electric-sql/pglite'; +import { vector } from '@electric-sql/pglite/vector'; import { sql } from 'drizzle-orm'; import { PgliteDatabase, drizzle } from 'drizzle-orm/pglite'; import { Md5 } from 'ts-md5'; -import { sleep } from '@/utils/sleep'; - import migrations from '../core/migrations.json'; import { DrizzleMigrationModel } from '../models/drizzleMigration'; import * as schema from '../schemas'; const pgliteSchemaHashCache = 'LOBE_CHAT_PGLITE_SCHEMA_HASH'; - const DB_NAME = 'lobechat'; + type DrizzleInstance = PgliteDatabase; -interface onErrorState { - error: Error; - migrationTableItems: MigrationTableItem[]; - migrationsSQL: MigrationSQL[]; -} - -export interface DatabaseLoadingCallbacks { - onError?: (error: onErrorState) => void; - onProgress?: (progress: ClientDBLoadingProgress) => void; - onStateChange?: (state: DatabaseLoadingState) => void; -} - -export class DatabaseManager { +class DatabaseManager { private static instance: DatabaseManager; private dbInstance: DrizzleInstance | null = null; private initPromise: Promise | null = null; - private callbacks?: DatabaseLoadingCallbacks; private isLocalDBSchemaSynced = false; - // CDN configuration - private static WASM_CDN_URL = - 'https://registry.npmmirror.com/@electric-sql/pglite/0.2.17/files/dist/postgres.wasm'; - - private static FSBUNDLER_CDN_URL = - 'https://registry.npmmirror.com/@electric-sql/pglite/0.2.17/files/dist/postgres.data'; - - private static VECTOR_CDN_URL = - 'https://registry.npmmirror.com/@electric-sql/pglite/0.2.17/files/dist/vector.tar.gz'; - private constructor() {} static getInstance() { @@ -57,108 +28,8 @@ export class DatabaseManager { return DatabaseManager.instance; } - // Load and compile WASM module - private async loadWasmModule(): Promise { - const start = Date.now(); - this.callbacks?.onStateChange?.(DatabaseLoadingState.LoadingWasm); - - const response = await fetch(DatabaseManager.WASM_CDN_URL); - - const contentLength = Number(response.headers.get('Content-Length')) || 0; - const reader = response.body?.getReader(); - - if (!reader) throw new Error('Failed to start WASM download'); - - let receivedLength = 0; - const chunks: Uint8Array[] = []; - - // Read data stream - // eslint-disable-next-line no-constant-condition - while (true) { - const { done, value } = await reader.read(); - - if (done) break; - - chunks.push(value); - receivedLength += value.length; - - // Calculate and report progress - const progress = Math.min(Math.round((receivedLength / contentLength) * 100), 100); - this.callbacks?.onProgress?.({ - phase: 'wasm', - progress, - }); - } - - // Merge data chunks - const wasmBytes = new Uint8Array(receivedLength); - let position = 0; - for (const chunk of chunks) { - wasmBytes.set(chunk, position); - position += chunk.length; - } - - this.callbacks?.onProgress?.({ - costTime: Date.now() - start, - phase: 'wasm', - progress: 100, - }); - - // Compile WASM module - return WebAssembly.compile(wasmBytes); - } - - private fetchFsBundle = async () => { - const res = await fetch(DatabaseManager.FSBUNDLER_CDN_URL); - - return await res.blob(); - }; - - // Asynchronously load PGlite related dependencies - private async loadDependencies() { - const start = Date.now(); - this.callbacks?.onStateChange?.(DatabaseLoadingState.LoadingDependencies); - - const imports = [ - import('@electric-sql/pglite').then((m) => ({ - IdbFs: m.IdbFs, - MemoryFS: m.MemoryFS, - PGlite: m.PGlite, - })), - import('@electric-sql/pglite/vector'), - this.fetchFsBundle(), - ]; - - let loaded = 0; - const results = await Promise.all( - imports.map(async (importPromise) => { - const result = await importPromise; - loaded += 1; - - // Calculate loading progress - this.callbacks?.onProgress?.({ - phase: 'dependencies', - progress: Math.min(Math.round((loaded / imports.length) * 100), 100), - }); - return result; - }), - ); - - this.callbacks?.onProgress?.({ - costTime: Date.now() - start, - phase: 'dependencies', - progress: 100, - }); - - // @ts-ignore - const [{ PGlite, IdbFs, MemoryFS }, { vector }, fsBundle] = results; - - return { IdbFs, MemoryFS, PGlite, fsBundle, vector }; - } - - // Database migration method - private async migrate(skipMultiRun = false): Promise { - if (this.isLocalDBSchemaSynced && skipMultiRun) return this.db; + private async migrate(): Promise { + if (this.isLocalDBSchemaSynced) return this.db; let hash: string | undefined; if (typeof localStorage !== 'undefined') { @@ -179,17 +50,13 @@ export class DatabaseManager { } } catch (error) { console.warn('Error checking table existence, proceeding with migration', error); - // If query fails, continue migration to ensure safety } } } const start = Date.now(); try { - this.callbacks?.onStateChange?.(DatabaseLoadingState.Migrating); - - // refs: https://github.com/drizzle-team/drizzle-orm/discussions/2532 - // @ts-expect-error + // @ts-expect-error - migrate internal API await this.db.dialect.migrate(migrations, this.db.session, {}); if (typeof localStorage !== 'undefined' && hash) { @@ -197,7 +64,6 @@ export class DatabaseManager { } this.isLocalDBSchemaSynced = true; - console.info(`🗂 Migration success, take ${Date.now() - start}ms`); } catch (cause) { console.error('❌ Local database schema migration failed', cause); @@ -207,95 +73,32 @@ export class DatabaseManager { return this.db; } - // Initialize database - async initialize(callbacks?: DatabaseLoadingCallbacks): Promise { + async initialize(): Promise { if (this.initPromise) return this.initPromise; - this.callbacks = callbacks; - this.initPromise = (async () => { - try { - if (this.dbInstance) return this.dbInstance; + if (this.dbInstance) return this.dbInstance; - const time = Date.now(); - // Initialize database - this.callbacks?.onStateChange?.(DatabaseLoadingState.Initializing); + const time = Date.now(); - // Load dependencies - const { fsBundle, PGlite, MemoryFS, IdbFs, vector } = await this.loadDependencies(); + // 直接使用 pglite,自动处理 wasm 加载 + const pglite = new PGlite(`idb://${DB_NAME}`, { + extensions: { vector }, + relaxedDurability: true, + }); - // Load and compile WASM module - const wasmModule = await this.loadWasmModule(); + this.dbInstance = drizzle({ client: pglite, schema }); - const { initPgliteWorker } = await import('./pglite'); + await this.migrate(); - let db: typeof PGlite; + console.log(`✅ Database initialized in ${Date.now() - time}ms`); - // make db as web worker if worker is available - // https://github.com/lobehub/lobe-chat/issues/5785 - if (typeof Worker !== 'undefined' && typeof navigator.locks !== 'undefined') { - db = await initPgliteWorker({ - dbName: DB_NAME, - fsBundle: fsBundle as Blob, - vectorBundlePath: DatabaseManager.VECTOR_CDN_URL, - wasmModule, - }); - } else { - // in edge runtime or test runtime, we don't have worker - db = new PGlite({ - extensions: { vector }, - fs: typeof window === 'undefined' ? new MemoryFS(DB_NAME) : new IdbFs(DB_NAME), - relaxedDurability: true, - wasmModule, - }); - } - - this.dbInstance = drizzle({ client: db, schema }); - - await this.migrate(true); - - this.callbacks?.onStateChange?.(DatabaseLoadingState.Finished); - console.log(`✅ Database initialized in ${Date.now() - time}ms`); - - await sleep(50); - - this.callbacks?.onStateChange?.(DatabaseLoadingState.Ready); - - return this.dbInstance as DrizzleInstance; - } catch (e) { - this.initPromise = null; - this.callbacks?.onStateChange?.(DatabaseLoadingState.Error); - const error = e as Error; - - // Query migration table data - let migrationsTableData: MigrationTableItem[] = []; - try { - // Attempt to query migration table - const drizzleMigration = new DrizzleMigrationModel(this.db as any); - migrationsTableData = await drizzleMigration.getMigrationList(); - } catch (queryError) { - console.error('Failed to query migrations table:', queryError); - } - - this.callbacks?.onError?.({ - error: { - message: error.message, - name: error.name, - stack: error.stack, - }, - migrationTableItems: migrationsTableData, - migrationsSQL: migrations, - }); - - console.error(error); - throw error; - } + return this.dbInstance; })(); return this.initPromise; } - // Get database instance get db(): DrizzleInstance { if (!this.dbInstance) { throw new Error('Database not initialized. Please call initialize() first.'); @@ -303,7 +106,6 @@ export class DatabaseManager { return this.dbInstance; } - // Create proxy object createProxy(): DrizzleInstance { return new Proxy({} as DrizzleInstance, { get: (target, prop) => { @@ -313,7 +115,7 @@ export class DatabaseManager { } async resetDatabase(): Promise { - // 1. Close existing PGlite connection (if exists) + // 1. Close existing PGlite connection if (this.dbInstance) { try { // @ts-ignore @@ -321,31 +123,28 @@ export class DatabaseManager { console.log('PGlite instance closed successfully.'); } catch (e) { console.error('Error closing PGlite instance:', e); - // Even if closing fails, continue with deletion attempt; IndexedDB onblocked or onerror will handle subsequent issues } } // 2. Reset database instance and initialization state this.dbInstance = null; this.initPromise = null; - this.isLocalDBSchemaSynced = false; // Reset sync state + this.isLocalDBSchemaSynced = false; // 3. Delete IndexedDB database return new Promise((resolve, reject) => { - // Check if IndexedDB is available if (typeof indexedDB === 'undefined') { console.warn('IndexedDB is not available, cannot delete database'); - resolve(); // Cannot delete in this environment, resolve directly + resolve(); return; } - const dbName = `/pglite/${DB_NAME}`; // Path used by PGlite IdbFs + const dbName = `/pglite/${DB_NAME}`; const request = indexedDB.deleteDatabase(dbName); request.onsuccess = () => { console.log(`✅ Database '${dbName}' reset successfully`); - // Clear locally stored schema hash if (typeof localStorage !== 'undefined') { localStorage.removeItem(pgliteSchemaHashCache); } @@ -365,14 +164,10 @@ export class DatabaseManager { }; request.onblocked = (event) => { - // This event is triggered when other open connections block database deletion - console.warn( - `Deletion of database '${dbName}' is blocked. This usually means other connections (e.g., in other tabs) are still open. Event:`, - event, - ); + console.warn(`Deletion of database '${dbName}' is blocked.`, event); reject( new Error( - `Failed to reset database '${dbName}' because it is blocked by other open connections. Please close other tabs or applications using this database and try again.`, + `Failed to reset database '${dbName}' because it is blocked by other open connections.`, ), ); }; @@ -383,12 +178,9 @@ export class DatabaseManager { // Export singleton const dbManager = DatabaseManager.getInstance(); -// Keep original clientDB export unchanged export const clientDB = dbManager.createProxy(); -// Export initialization method for application startup -export const initializeDB = (callbacks?: DatabaseLoadingCallbacks) => - dbManager.initialize(callbacks); +export const initializeDB = () => dbManager.initialize(); export const resetClientDatabase = async () => { await dbManager.resetDatabase(); diff --git a/packages/database/src/client/pglite.ts b/packages/database/src/client/pglite.ts deleted file mode 100644 index ff7bce695f..0000000000 --- a/packages/database/src/client/pglite.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PGliteWorker } from '@electric-sql/pglite/worker'; - -import { InitMeta } from './type'; - -export const initPgliteWorker = async (meta: InitMeta) => { - const worker = await PGliteWorker.create( - new Worker(new URL('pglite.worker.ts', import.meta.url)), - { meta }, - ); - - // Listen for worker status changes - worker.onLeaderChange(() => { - console.log('Worker leader changed, isLeader:', worker?.isLeader); - }); - - return worker as PGliteWorker; -}; diff --git a/packages/database/src/client/pglite.worker.ts b/packages/database/src/client/pglite.worker.ts deleted file mode 100644 index e65fdb4de1..0000000000 --- a/packages/database/src/client/pglite.worker.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { worker } from '@electric-sql/pglite/worker'; - -import { InitMeta } from './type'; - -worker({ - async init(options) { - const { wasmModule, fsBundle, vectorBundlePath, dbName } = options.meta as InitMeta; - const { PGlite } = await import('@electric-sql/pglite'); - - return new PGlite({ - dataDir: `idb://${dbName}`, - extensions: { - vector: { - name: 'pgvector', - setup: async (pglite, options) => { - return { bundlePath: new URL(vectorBundlePath), options }; - }, - }, - }, - fsBundle, - relaxedDurability: true, - wasmModule, - }); - }, -}); diff --git a/packages/database/src/models/__tests__/_util.ts b/packages/database/src/models/__tests__/_util.ts index 97b10443dd..0e95d894b6 100644 --- a/packages/database/src/models/__tests__/_util.ts +++ b/packages/database/src/models/__tests__/_util.ts @@ -1,14 +1,30 @@ -import { clientDB, initializeDB } from '../../client/db'; +import { PGlite } from '@electric-sql/pglite'; +import { vector } from '@electric-sql/pglite/vector'; +import { drizzle } from 'drizzle-orm/pglite'; + +import migrations from '../../core/migrations.json'; +import * as schema from '../../schemas'; import { LobeChatDatabase } from '../../type'; const isServerDBMode = process.env.TEST_SERVER_DB === '1'; +let testClientDB: ReturnType> | null = null; + export const getTestDB = async () => { if (isServerDBMode) { const { getTestDBInstance } = await import('../../core/dbForTest'); return await getTestDBInstance(); } - await initializeDB(); - return clientDB as LobeChatDatabase; + if (testClientDB) return testClientDB as unknown as LobeChatDatabase; + + // 直接使用 pglite 内置资源,不需要从 CDN 下载 + const pglite = new PGlite({ extensions: { vector } }); + + testClientDB = drizzle({ client: pglite, schema }); + + // @ts-expect-error - migrate internal API + await testClientDB.dialect.migrate(migrations, testClientDB.session, {}); + + return testClientDB as unknown as LobeChatDatabase; }; diff --git a/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx b/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx index 49d7bac983..b43ede9c28 100644 --- a/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +++ b/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx @@ -22,13 +22,14 @@ import ModelConfigModal from './ModelConfigModal'; import { ProviderSettingsContext } from './ProviderSettingsContext'; const styles = createStaticStyles(({ css, cx }) => { - const config = css` - opacity: 0; - transition: all 100ms ease-in-out; - `; - return { - config, + config: cx( + 'model-item-config', + css` + opacity: 0; + transition: all 100ms ease-in-out; + `, + ), container: css` position: relative; border-radius: ${cssVar.borderRadiusLG}px; @@ -37,7 +38,7 @@ const styles = createStaticStyles(({ css, cx }) => { &:hover { background-color: ${cssVar.colorFillTertiary}; - .${cx(config)} { + .model-item-config { opacity: 1; } } diff --git a/src/libs/next/config/define-config.ts b/src/libs/next/config/define-config.ts index d9b6eacf95..8d1a96a7b8 100644 --- a/src/libs/next/config/define-config.ts +++ b/src/libs/next/config/define-config.ts @@ -311,7 +311,7 @@ export function defineConfig(config: CustomNextConfig) { ], // when external packages in dev mode with turbopack, this config will lead to bundle error - serverExternalPackages: isProd ? ['@electric-sql/pglite', 'pdfkit'] : ['pdfkit'], + serverExternalPackages: ['pdfkit'], transpilePackages: ['pdfjs-dist', 'mermaid', 'better-auth-harmony'], turbopack: {