mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-23 23:20:55 +08:00
feat: 支持数据库升级
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
- 🔒 **保护隐私**:聊天记录和配置都存在你的本地数据库,所有分析都在本地进行(AI 功能例外)。
|
||||
- 🤖 **智能 AI Agent**:集成 10+ Function Calling 工具,支持动态调度,深度挖掘聊天记录中的更多有趣。
|
||||
- 📊 **多维数据可视化**:提供活跃度趋势、时间规律分布、成员排行等多个维度的直观分析图表。
|
||||
- 🧩 **格式标准化**:通过强大的数据抽象层,抹平不同社交平台的格式差异,任何聊天记录都能分析。
|
||||
- 🧩 **格式标准化**:通过强大的数据抽象层,抹平不同聊天软件的格式差异,任何聊天记录都能分析。
|
||||
|
||||
## 使用指南
|
||||
|
||||
|
||||
+124
-110
@@ -8,6 +8,7 @@ import { app } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import type { DbMeta, ParseResult, AnalysisSession } from '../../../src/types/base'
|
||||
import { migrateDatabase, needsMigration, CURRENT_SCHEMA_VERSION } from './migrations'
|
||||
|
||||
// 数据库存储目录
|
||||
let DB_DIR: string | null = null
|
||||
@@ -72,7 +73,9 @@ function createDatabase(sessionId: string): Database.Database {
|
||||
type TEXT NOT NULL,
|
||||
imported_at INTEGER NOT NULL,
|
||||
group_id TEXT,
|
||||
group_avatar TEXT
|
||||
group_avatar TEXT,
|
||||
owner_id TEXT,
|
||||
schema_version INTEGER DEFAULT ${CURRENT_SCHEMA_VERSION}
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member (
|
||||
@@ -115,17 +118,37 @@ function createDatabase(sessionId: string): Database.Database {
|
||||
|
||||
/**
|
||||
* 打开已存在的数据库
|
||||
* @param readonly 是否只读模式(默认 true)
|
||||
*/
|
||||
export function openDatabase(sessionId: string): Database.Database | null {
|
||||
export function openDatabase(sessionId: string, readonly = true): Database.Database | null {
|
||||
const dbPath = getDbPath(sessionId)
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
return null
|
||||
}
|
||||
const db = new Database(dbPath, { readonly: true })
|
||||
const db = new Database(dbPath, { readonly })
|
||||
db.pragma('journal_mode = WAL')
|
||||
return db
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开数据库并执行迁移(如果需要)
|
||||
* 用于需要写入的场景
|
||||
*/
|
||||
export function openDatabaseWithMigration(sessionId: string): Database.Database | null {
|
||||
const dbPath = getDbPath(sessionId)
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const db = new Database(dbPath)
|
||||
db.pragma('journal_mode = WAL')
|
||||
|
||||
// 执行迁移
|
||||
migrateDatabase(db)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入解析后的数据到数据库
|
||||
*/
|
||||
@@ -137,8 +160,8 @@ export function importData(parseResult: ParseResult): string {
|
||||
try {
|
||||
const importTransaction = db.transaction(() => {
|
||||
const insertMeta = db.prepare(`
|
||||
INSERT INTO meta (name, platform, type, imported_at, group_id, group_avatar)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO meta (name, platform, type, imported_at, group_id, group_avatar, owner_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
insertMeta.run(
|
||||
parseResult.meta.name,
|
||||
@@ -146,7 +169,8 @@ export function importData(parseResult: ParseResult): string {
|
||||
parseResult.meta.type,
|
||||
Math.floor(Date.now() / 1000),
|
||||
parseResult.meta.groupId || null,
|
||||
parseResult.meta.groupAvatar || null
|
||||
parseResult.meta.groupAvatar || null,
|
||||
parseResult.meta.ownerId || null
|
||||
)
|
||||
|
||||
const insertMember = db.prepare(`
|
||||
@@ -159,7 +183,12 @@ export function importData(parseResult: ParseResult): string {
|
||||
const memberIdMap = new Map<string, number>()
|
||||
|
||||
for (const member of parseResult.members) {
|
||||
insertMember.run(member.platformId, member.accountName || null, member.groupNickname || null, member.avatar || null)
|
||||
insertMember.run(
|
||||
member.platformId,
|
||||
member.accountName || null,
|
||||
member.groupNickname || null,
|
||||
member.avatar || null
|
||||
)
|
||||
const row = getMemberId.get(member.platformId) as { id: number }
|
||||
memberIdMap.set(member.platformId, row.id)
|
||||
}
|
||||
@@ -268,113 +297,22 @@ export function importData(parseResult: ParseResult): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分析会话列表
|
||||
* 更新会话的 ownerId
|
||||
*/
|
||||
export function getAllSessions(): AnalysisSession[] {
|
||||
ensureDbDir()
|
||||
const sessions: AnalysisSession[] = []
|
||||
const dbDir = getDbDir()
|
||||
const allFiles = fs.readdirSync(dbDir)
|
||||
const files = allFiles.filter((f) => f.endsWith('.db'))
|
||||
|
||||
for (const file of files) {
|
||||
const sessionId = file.replace('.db', '')
|
||||
const dbPath = getDbPath(sessionId)
|
||||
|
||||
try {
|
||||
const db = new Database(dbPath)
|
||||
db.pragma('journal_mode = WAL')
|
||||
|
||||
const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined
|
||||
|
||||
if (meta) {
|
||||
const messageCount = (
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM message msg
|
||||
JOIN member m ON msg.sender_id = m.id
|
||||
WHERE COALESCE(m.account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
const memberCount = (
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM member
|
||||
WHERE COALESCE(account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
|
||||
sessions.push({
|
||||
id: sessionId,
|
||||
name: meta.name,
|
||||
platform: meta.platform as AnalysisSession['platform'],
|
||||
type: meta.type as AnalysisSession['type'],
|
||||
importedAt: meta.imported_at,
|
||||
messageCount,
|
||||
memberCount,
|
||||
dbPath,
|
||||
groupId: meta.group_id || null,
|
||||
groupAvatar: meta.group_avatar || null,
|
||||
})
|
||||
}
|
||||
|
||||
db.close()
|
||||
} catch (error) {
|
||||
console.error(`[Database] Failed to read database \${file}:`, error)
|
||||
}
|
||||
export function updateSessionOwnerId(sessionId: string, ownerId: string | null): boolean {
|
||||
// 使用带迁移的打开方式,确保 owner_id 列存在
|
||||
const db = openDatabaseWithMigration(sessionId)
|
||||
if (!db) {
|
||||
return false
|
||||
}
|
||||
|
||||
return sessions.sort((a, b) => b.importedAt - a.importedAt)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个会话信息
|
||||
*/
|
||||
export function getSession(sessionId: string): AnalysisSession | null {
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return null
|
||||
|
||||
try {
|
||||
const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined
|
||||
if (!meta) return null
|
||||
|
||||
const messageCount = (
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM message msg
|
||||
JOIN member m ON msg.sender_id = m.id
|
||||
WHERE COALESCE(m.account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
|
||||
const memberCount = (
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM member
|
||||
WHERE COALESCE(account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
|
||||
return {
|
||||
id: sessionId,
|
||||
name: meta.name,
|
||||
platform: meta.platform as AnalysisSession['platform'],
|
||||
type: meta.type as AnalysisSession['type'],
|
||||
importedAt: meta.imported_at,
|
||||
messageCount,
|
||||
memberCount,
|
||||
dbPath: getDbPath(sessionId),
|
||||
groupId: meta.group_id || null,
|
||||
groupAvatar: meta.group_avatar || null,
|
||||
}
|
||||
const stmt = db.prepare('UPDATE meta SET owner_id = ?')
|
||||
stmt.run(ownerId)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('[Database] Failed to update session ownerId:', error)
|
||||
return false
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
@@ -436,3 +374,79 @@ export function getDbDirectory(): string {
|
||||
ensureDbDir()
|
||||
return getDbDir()
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有数据库需要迁移
|
||||
* @returns 需要迁移的数据库数量和最低版本
|
||||
*/
|
||||
export function checkMigrationNeeded(): { count: number; sessionIds: string[]; lowestVersion: number } {
|
||||
ensureDbDir()
|
||||
const dbDir = getDbDir()
|
||||
const files = fs.readdirSync(dbDir).filter((f) => f.endsWith('.db'))
|
||||
const needsMigrationList: string[] = []
|
||||
let lowestVersion = CURRENT_SCHEMA_VERSION
|
||||
|
||||
for (const file of files) {
|
||||
const sessionId = file.replace('.db', '')
|
||||
const dbPath = getDbPath(sessionId)
|
||||
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true })
|
||||
db.pragma('journal_mode = WAL')
|
||||
|
||||
if (needsMigration(db)) {
|
||||
needsMigrationList.push(sessionId)
|
||||
// 获取这个数据库的版本
|
||||
const tableInfo = db.prepare('PRAGMA table_info(meta)').all() as Array<{ name: string }>
|
||||
const hasVersionColumn = tableInfo.some((col) => col.name === 'schema_version')
|
||||
let dbVersion = 0
|
||||
if (hasVersionColumn) {
|
||||
const result = db.prepare('SELECT schema_version FROM meta LIMIT 1').get() as
|
||||
| { schema_version: number | null }
|
||||
| undefined
|
||||
dbVersion = result?.schema_version ?? 0
|
||||
}
|
||||
lowestVersion = Math.min(lowestVersion, dbVersion)
|
||||
}
|
||||
|
||||
db.close()
|
||||
} catch (error) {
|
||||
console.error(`[Database] Failed to check migration for ${file}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return { count: needsMigrationList.length, sessionIds: needsMigrationList, lowestVersion }
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行所有数据库的迁移
|
||||
* @returns 迁移结果
|
||||
*/
|
||||
export function migrateAllDatabases(): { success: boolean; migratedCount: number; error?: string } {
|
||||
const { sessionIds } = checkMigrationNeeded()
|
||||
|
||||
if (sessionIds.length === 0) {
|
||||
return { success: true, migratedCount: 0 }
|
||||
}
|
||||
|
||||
let migratedCount = 0
|
||||
|
||||
for (const sessionId of sessionIds) {
|
||||
try {
|
||||
const db = openDatabaseWithMigration(sessionId)
|
||||
if (db) {
|
||||
db.close()
|
||||
migratedCount++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Database] Failed to migrate ${sessionId}:`, error)
|
||||
return {
|
||||
success: false,
|
||||
migratedCount,
|
||||
error: `迁移 ${sessionId} 失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, migratedCount }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 数据库迁移系统
|
||||
*
|
||||
* 用于管理数据库 schema 的版本升级。
|
||||
* 每次添加新字段或修改表结构时,创建一个新的迁移脚本。
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在 migrations 数组中添加新的迁移对象
|
||||
* 2. version 必须递增
|
||||
* 3. up 函数执行迁移逻辑
|
||||
*/
|
||||
|
||||
import type Database from 'better-sqlite3'
|
||||
|
||||
/** 迁移脚本接口 */
|
||||
interface Migration {
|
||||
/** 版本号(必须递增) */
|
||||
version: number
|
||||
/** 迁移描述(技术说明) */
|
||||
description: string
|
||||
/** 用户可读的升级原因(显示在弹窗中) */
|
||||
userMessage: string
|
||||
/** 迁移执行函数 */
|
||||
up: (db: Database.Database) => void
|
||||
}
|
||||
|
||||
/** 导出给前端使用的迁移信息 */
|
||||
export interface MigrationInfo {
|
||||
version: number
|
||||
/** 技术描述(面向开发者) */
|
||||
description: string
|
||||
/** 用户可读的升级原因(显示在弹窗中) */
|
||||
userMessage: string
|
||||
}
|
||||
|
||||
/** 当前 schema 版本(最新迁移的版本号) */
|
||||
export const CURRENT_SCHEMA_VERSION = 1
|
||||
|
||||
/**
|
||||
* 迁移脚本列表
|
||||
* 注意:版本号必须递增,每个迁移只执行一次
|
||||
*/
|
||||
const migrations: Migration[] = [
|
||||
{
|
||||
version: 1,
|
||||
description: '添加 owner_id 字段到 meta 表',
|
||||
userMessage: '支持「Owner」功能,可在成员列表中设置自己的身份',
|
||||
up: (db) => {
|
||||
// 检查 owner_id 列是否已存在(防止重复执行)
|
||||
const tableInfo = db.prepare('PRAGMA table_info(meta)').all() as Array<{ name: string }>
|
||||
const hasOwnerIdColumn = tableInfo.some((col) => col.name === 'owner_id')
|
||||
if (!hasOwnerIdColumn) {
|
||||
db.exec('ALTER TABLE meta ADD COLUMN owner_id TEXT')
|
||||
}
|
||||
},
|
||||
},
|
||||
// 未来的迁移示例:
|
||||
// {
|
||||
// version: 2,
|
||||
// description: '添加 xxx 字段',
|
||||
// userMessage: '新功能说明',
|
||||
// up: (db) => {
|
||||
// db.exec('ALTER TABLE xxx ADD COLUMN yyy TEXT')
|
||||
// },
|
||||
// },
|
||||
]
|
||||
|
||||
/**
|
||||
* 获取数据库的 schema 版本
|
||||
* 如果没有版本信息,返回 0
|
||||
*/
|
||||
function getSchemaVersion(db: Database.Database): number {
|
||||
try {
|
||||
// 检查 meta 表是否有 schema_version 列
|
||||
const tableInfo = db.prepare('PRAGMA table_info(meta)').all() as Array<{ name: string }>
|
||||
const hasVersionColumn = tableInfo.some((col) => col.name === 'schema_version')
|
||||
|
||||
if (!hasVersionColumn) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const result = db.prepare('SELECT schema_version FROM meta LIMIT 1').get() as
|
||||
| { schema_version: number | null }
|
||||
| undefined
|
||||
return result?.schema_version ?? 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置数据库的 schema 版本
|
||||
*/
|
||||
function setSchemaVersion(db: Database.Database, version: number): void {
|
||||
// 检查 schema_version 列是否存在
|
||||
const tableInfo = db.prepare('PRAGMA table_info(meta)').all() as Array<{ name: string }>
|
||||
const hasVersionColumn = tableInfo.some((col) => col.name === 'schema_version')
|
||||
|
||||
if (!hasVersionColumn) {
|
||||
// 添加 schema_version 列
|
||||
db.exec('ALTER TABLE meta ADD COLUMN schema_version INTEGER DEFAULT 0')
|
||||
}
|
||||
|
||||
// 更新版本号
|
||||
db.prepare('UPDATE meta SET schema_version = ?').run(version)
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据库迁移
|
||||
* 自动检测当前版本并执行所有需要的迁移
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @returns 是否执行了迁移
|
||||
*/
|
||||
export function migrateDatabase(db: Database.Database): boolean {
|
||||
const currentVersion = getSchemaVersion(db)
|
||||
|
||||
// 已是最新版本,无需迁移
|
||||
if (currentVersion >= CURRENT_SCHEMA_VERSION) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取需要执行的迁移
|
||||
const pendingMigrations = migrations.filter((m) => m.version > currentVersion)
|
||||
|
||||
if (pendingMigrations.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Migration] 数据库版本 ${currentVersion} -> ${CURRENT_SCHEMA_VERSION},执行 ${pendingMigrations.length} 个迁移`
|
||||
)
|
||||
|
||||
// 在事务中执行所有迁移
|
||||
const migrate = db.transaction(() => {
|
||||
for (const migration of pendingMigrations) {
|
||||
console.log(`[Migration] 执行迁移 v${migration.version}: ${migration.description}`)
|
||||
migration.up(db)
|
||||
setSchemaVersion(db, migration.version)
|
||||
}
|
||||
})
|
||||
|
||||
migrate()
|
||||
|
||||
console.log(`[Migration] 迁移完成,当前版本: ${CURRENT_SCHEMA_VERSION}`)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据库是否需要迁移
|
||||
*/
|
||||
export function needsMigration(db: Database.Database): boolean {
|
||||
const currentVersion = getSchemaVersion(db)
|
||||
return currentVersion < CURRENT_SCHEMA_VERSION
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待执行的迁移信息(用户可读)
|
||||
* @param fromVersion 起始版本(不含)
|
||||
*/
|
||||
export function getPendingMigrationInfos(fromVersion = 0): MigrationInfo[] {
|
||||
return migrations
|
||||
.filter((m) => m.version > fromVersion)
|
||||
.map((m) => ({
|
||||
version: m.version,
|
||||
description: m.description,
|
||||
userMessage: m.userMessage,
|
||||
}))
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import * as worker from '../worker/workerManager'
|
||||
import * as parser from '../parser'
|
||||
import { detectFormat, type ParseProgress } from '../parser'
|
||||
import type { IpcContext } from './types'
|
||||
import { CURRENT_SCHEMA_VERSION, getPendingMigrationInfos, type MigrationInfo } from '../database/migrations'
|
||||
|
||||
/**
|
||||
* 注册聊天记录相关 IPC 处理器
|
||||
@@ -15,6 +16,41 @@ import type { IpcContext } from './types'
|
||||
export function registerChatHandlers(ctx: IpcContext): void {
|
||||
const { win } = ctx
|
||||
|
||||
// ==================== 数据库迁移 ====================
|
||||
|
||||
/**
|
||||
* 检查是否需要数据库迁移
|
||||
*/
|
||||
ipcMain.handle('chat:checkMigration', async () => {
|
||||
try {
|
||||
const result = databaseCore.checkMigrationNeeded()
|
||||
// 获取待执行的迁移信息(从最低版本开始)
|
||||
const pendingMigrations = getPendingMigrationInfos(result.lowestVersion)
|
||||
return {
|
||||
needsMigration: result.count > 0,
|
||||
count: result.count,
|
||||
currentVersion: CURRENT_SCHEMA_VERSION,
|
||||
pendingMigrations,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[IpcMain] 检查迁移失败:', error)
|
||||
return { needsMigration: false, count: 0, currentVersion: CURRENT_SCHEMA_VERSION, pendingMigrations: [] }
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 执行数据库迁移
|
||||
*/
|
||||
ipcMain.handle('chat:runMigration', async () => {
|
||||
try {
|
||||
const result = databaseCore.migrateAllDatabases()
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('[IpcMain] 执行迁移失败:', error)
|
||||
return { success: false, migratedCount: 0, error: String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 聊天记录导入与分析 ====================
|
||||
|
||||
/**
|
||||
@@ -522,6 +558,21 @@ export function registerChatHandlers(ctx: IpcContext): void {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新会话的所有者(ownerId)
|
||||
*/
|
||||
ipcMain.handle('chat:updateSessionOwnerId', async (_, sessionId: string, ownerId: string | null) => {
|
||||
try {
|
||||
// 先关闭数据库连接
|
||||
await worker.closeDatabase(sessionId)
|
||||
// 执行更新
|
||||
return databaseCore.updateSessionOwnerId(sessionId, ownerId)
|
||||
} catch (error) {
|
||||
console.error('更新会话所有者失败:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== SQL 实验室 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,29 @@ const api = {
|
||||
|
||||
// Chat Analysis API
|
||||
const chatApi = {
|
||||
// ==================== 数据库迁移 ====================
|
||||
|
||||
/**
|
||||
* 检查是否需要数据库迁移
|
||||
*/
|
||||
checkMigration: (): Promise<{
|
||||
needsMigration: boolean
|
||||
count: number
|
||||
currentVersion: number
|
||||
pendingMigrations: Array<{ version: number; userMessage: string }>
|
||||
}> => {
|
||||
return ipcRenderer.invoke('chat:checkMigration')
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行数据库迁移
|
||||
*/
|
||||
runMigration: (): Promise<{ success: boolean; migratedCount: number; error?: string }> => {
|
||||
return ipcRenderer.invoke('chat:runMigration')
|
||||
},
|
||||
|
||||
// ==================== 聊天记录导入与分析 ====================
|
||||
|
||||
/**
|
||||
* 选择聊天记录文件
|
||||
*/
|
||||
@@ -318,6 +341,14 @@ const chatApi = {
|
||||
return ipcRenderer.invoke('chat:deleteMember', sessionId, memberId)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新会话的所有者(ownerId)
|
||||
* @param ownerId 成员的 platformId,设置为 null 则清除
|
||||
*/
|
||||
updateSessionOwnerId: (sessionId: string, ownerId: string | null): Promise<boolean> => {
|
||||
return ipcRenderer.invoke('chat:updateSessionOwnerId', sessionId, ownerId)
|
||||
},
|
||||
|
||||
// ==================== SQL 实验室 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const sessionStore = useSessionStore()
|
||||
const { migrationCount, pendingMigrations, isMigrating } = storeToRefs(sessionStore)
|
||||
|
||||
// 弹窗状态
|
||||
const showModal = ref(false)
|
||||
const migrationError = ref<string | null>(null)
|
||||
|
||||
// 执行迁移
|
||||
async function handleMigration() {
|
||||
migrationError.value = null
|
||||
const result = await sessionStore.runMigration()
|
||||
if (result.success) {
|
||||
showModal.value = false
|
||||
// 迁移完成后重新加载会话列表
|
||||
await sessionStore.loadSessions()
|
||||
} else {
|
||||
migrationError.value = result.error || '迁移失败'
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时检查是否需要迁移
|
||||
onMounted(async () => {
|
||||
const result = await sessionStore.checkMigration()
|
||||
if (result.needsMigration) {
|
||||
showModal.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="showModal" :ui="{ content: 'max-w-md' }" :prevent-close="true">
|
||||
<template #content>
|
||||
<div class="p-6 text-center">
|
||||
<div
|
||||
class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30"
|
||||
>
|
||||
<UIcon name="i-heroicons-arrow-up-circle" class="h-7 w-7 text-blue-500" />
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">数据库需要升级</h3>
|
||||
<p class="mb-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
检测到 {{ migrationCount }} 个数据库需要升级以支持新功能。
|
||||
<br />
|
||||
升级过程通常只需几秒钟,不会丢失任何数据。
|
||||
</p>
|
||||
|
||||
<!-- 升级内容列表 -->
|
||||
<div v-if="pendingMigrations.length > 0" class="mb-4 rounded-lg bg-gray-50 p-3 text-left dark:bg-gray-800">
|
||||
<p class="mb-2 text-xs font-medium text-gray-500 dark:text-gray-400">本次升级内容:</p>
|
||||
<ul class="space-y-2">
|
||||
<li v-for="migration in pendingMigrations" :key="migration.version" class="flex items-start gap-2">
|
||||
<UIcon name="i-heroicons-check-circle" class="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">{{ migration.userMessage }}</p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{{ migration.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div
|
||||
v-if="migrationError"
|
||||
class="mb-4 rounded-lg bg-red-50 p-3 text-left text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-heroicons-exclamation-circle" class="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{{ migrationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UButton color="primary" size="lg" :loading="isMigrating" class="w-full" @click="handleMigration">
|
||||
{{ isMigrating ? '正在升级...' : '立即升级' }}
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AgreementModal from './components/AgreementModal.vue'
|
||||
import MigrationModal from './components/MigrationModal.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const sessionStore = useSessionStore()
|
||||
@@ -250,5 +251,8 @@ function getProgressDetail(): string {
|
||||
|
||||
<!-- 用户协议弹窗 -->
|
||||
<AgreementModal />
|
||||
|
||||
<!-- 数据库迁移弹窗 -->
|
||||
<MigrationModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,23 @@ import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { AnalysisSession, ImportProgress } from '@/types/base'
|
||||
|
||||
/** 迁移信息 */
|
||||
export interface MigrationInfo {
|
||||
version: number
|
||||
/** 技术描述(面向开发者) */
|
||||
description: string
|
||||
/** 用户可读的升级原因(显示在弹窗中) */
|
||||
userMessage: string
|
||||
}
|
||||
|
||||
/** 迁移检查结果 */
|
||||
export interface MigrationCheckResult {
|
||||
needsMigration: boolean
|
||||
count: number
|
||||
currentVersion: number
|
||||
pendingMigrations: MigrationInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话与导入相关的全局状态
|
||||
*/
|
||||
@@ -24,6 +41,48 @@ export const useSessionStore = defineStore(
|
||||
return sessions.value.find((s) => s.id === currentSessionId.value) || null
|
||||
})
|
||||
|
||||
// 迁移相关状态
|
||||
const migrationNeeded = ref(false)
|
||||
const migrationCount = ref(0)
|
||||
const pendingMigrations = ref<MigrationInfo[]>([])
|
||||
const isMigrating = ref(false)
|
||||
|
||||
/**
|
||||
* 检查是否需要数据库迁移
|
||||
*/
|
||||
async function checkMigration(): Promise<MigrationCheckResult> {
|
||||
try {
|
||||
const result = await window.chatApi.checkMigration()
|
||||
migrationNeeded.value = result.needsMigration
|
||||
migrationCount.value = result.count
|
||||
pendingMigrations.value = result.pendingMigrations || []
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('检查迁移失败:', error)
|
||||
return { needsMigration: false, count: 0, currentVersion: 0, pendingMigrations: [] }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据库迁移
|
||||
*/
|
||||
async function runMigration(): Promise<{ success: boolean; error?: string }> {
|
||||
isMigrating.value = true
|
||||
try {
|
||||
const result = await window.chatApi.runMigration()
|
||||
if (result.success) {
|
||||
migrationNeeded.value = false
|
||||
migrationCount.value = 0
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('执行迁移失败:', error)
|
||||
return { success: false, error: String(error) }
|
||||
} finally {
|
||||
isMigrating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库加载会话列表
|
||||
*/
|
||||
@@ -199,6 +258,25 @@ export const useSessionStore = defineStore(
|
||||
currentSessionId.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话的所有者
|
||||
*/
|
||||
async function updateSessionOwnerId(id: string, ownerId: string | null): Promise<boolean> {
|
||||
try {
|
||||
const success = await window.chatApi.updateSessionOwnerId(id, ownerId)
|
||||
if (success) {
|
||||
const session = sessions.value.find((s) => s.id === id)
|
||||
if (session) {
|
||||
session.ownerId = ownerId
|
||||
}
|
||||
}
|
||||
return success
|
||||
} catch (error) {
|
||||
console.error('更新会话所有者失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sessions,
|
||||
currentSessionId,
|
||||
@@ -206,6 +284,14 @@ export const useSessionStore = defineStore(
|
||||
importProgress,
|
||||
isInitialized,
|
||||
currentSession,
|
||||
// 迁移相关
|
||||
migrationNeeded,
|
||||
migrationCount,
|
||||
pendingMigrations,
|
||||
isMigrating,
|
||||
checkMigration,
|
||||
runMigration,
|
||||
// 会话操作
|
||||
loadSessions,
|
||||
importFile,
|
||||
importFileFromPath,
|
||||
@@ -213,6 +299,7 @@ export const useSessionStore = defineStore(
|
||||
deleteSession,
|
||||
renameSession,
|
||||
clearSelection,
|
||||
updateSessionOwnerId,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user