mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(adapters): define QueryAdapter interface and ElectronAdapter
Phase 3.0.1 + 3.0.2: Introduce the adapter abstraction layer for multi-platform support (Electron / CLI serve Web / Online Web). - src/adapters/types.ts: QueryAdapter interface with 30+ methods covering sessions, statistics, members, social analysis, SQL Lab, and plugin system. AdapterCapabilities for feature flags. - src/adapters/electron.ts: ElectronAdapter wrapping window.chatApi with full capability support, zero behavior change for desktop. - src/adapters/index.ts: async factory with compile-time adapter selection via __IS_ELECTRON__ / __IS_BROWSER_STANDALONE__. - src/utils/platform.ts: add IS_BROWSER_STANDALONE detection. - src/env.d.ts: declare __IS_BROWSER_STANDALONE__ global.
This commit is contained in:
1 parent
847d75e4b6
commit
f6b3dabc31
5 files changed
+447
-2
No files matched your search
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* ElectronAdapter — 包装 window.chatApi (Electron IPC)
|
||||
*
|
||||
* 行为与现有 window.chatApi 完全一致,确保桌面版零影响。
|
||||
* 所有方法直接代理到 window.chatApi 的对应方法。
|
||||
*/
|
||||
|
||||
import type {
|
||||
QueryAdapter,
|
||||
AdapterCapabilities,
|
||||
SQLResult,
|
||||
TableSchema,
|
||||
PaginationParams,
|
||||
PaginatedResult,
|
||||
MentionGraphData,
|
||||
MessageLengthDistribution,
|
||||
} from './types'
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
import type {
|
||||
MemberActivity,
|
||||
MemberWithStats,
|
||||
MemberNameHistory,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
WeekdayActivity,
|
||||
MonthlyActivity,
|
||||
CatchphraseAnalysis,
|
||||
MentionAnalysis,
|
||||
LaughAnalysis,
|
||||
ClusterGraphData,
|
||||
ClusterGraphOptions,
|
||||
RelationshipStats,
|
||||
} from '@/types/analysis'
|
||||
import type { LanguagePreferenceResult } from '@/types/quotes/languagePreference'
|
||||
|
||||
export class ElectronAdapter implements QueryAdapter {
|
||||
getCapabilities(): AdapterCapabilities {
|
||||
return {
|
||||
ai: true,
|
||||
nlp: true,
|
||||
fts: true,
|
||||
nativeFileDialog: true,
|
||||
osIntegration: true,
|
||||
demoImport: true,
|
||||
plugin: true,
|
||||
migration: true,
|
||||
merge: true,
|
||||
incrementalImport: true,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 会话管理 ====================
|
||||
|
||||
getSessions(): Promise<AnalysisSession[]> {
|
||||
return window.chatApi.getSessions()
|
||||
}
|
||||
|
||||
getSession(sessionId: string): Promise<AnalysisSession | null> {
|
||||
return window.chatApi.getSession(sessionId)
|
||||
}
|
||||
|
||||
deleteSession(sessionId: string): Promise<boolean> {
|
||||
return window.chatApi.deleteSession(sessionId)
|
||||
}
|
||||
|
||||
renameSession(sessionId: string, newName: string): Promise<boolean> {
|
||||
return window.chatApi.renameSession(sessionId, newName)
|
||||
}
|
||||
|
||||
updateSessionOwnerId(sessionId: string, ownerId: string | null): Promise<boolean> {
|
||||
return window.chatApi.updateSessionOwnerId(sessionId, ownerId)
|
||||
}
|
||||
|
||||
// ==================== 时间范围 ====================
|
||||
|
||||
getAvailableYears(sessionId: string): Promise<number[]> {
|
||||
return window.chatApi.getAvailableYears(sessionId)
|
||||
}
|
||||
|
||||
getTimeRange(sessionId: string): Promise<{ start: number; end: number } | null> {
|
||||
return window.chatApi.getTimeRange(sessionId)
|
||||
}
|
||||
|
||||
// ==================== 统计分析 ====================
|
||||
|
||||
getMemberActivity(sessionId: string, filter?: TimeFilter): Promise<MemberActivity[]> {
|
||||
return window.chatApi.getMemberActivity(sessionId, filter)
|
||||
}
|
||||
|
||||
getHourlyActivity(sessionId: string, filter?: TimeFilter): Promise<HourlyActivity[]> {
|
||||
return window.chatApi.getHourlyActivity(sessionId, filter)
|
||||
}
|
||||
|
||||
getDailyActivity(sessionId: string, filter?: TimeFilter): Promise<DailyActivity[]> {
|
||||
return window.chatApi.getDailyActivity(sessionId, filter)
|
||||
}
|
||||
|
||||
getWeekdayActivity(sessionId: string, filter?: TimeFilter): Promise<WeekdayActivity[]> {
|
||||
return window.chatApi.getWeekdayActivity(sessionId, filter)
|
||||
}
|
||||
|
||||
getMonthlyActivity(sessionId: string, filter?: TimeFilter): Promise<MonthlyActivity[]> {
|
||||
return window.chatApi.getMonthlyActivity(sessionId, filter)
|
||||
}
|
||||
|
||||
getYearlyActivity(sessionId: string, filter?: TimeFilter): Promise<Array<{ year: number; messageCount: number }>> {
|
||||
return window.chatApi.getYearlyActivity(sessionId, filter)
|
||||
}
|
||||
|
||||
getMessageLengthDistribution(sessionId: string, filter?: TimeFilter): Promise<MessageLengthDistribution> {
|
||||
return window.chatApi.getMessageLengthDistribution(sessionId, filter)
|
||||
}
|
||||
|
||||
getMessageTypeDistribution(
|
||||
sessionId: string,
|
||||
filter?: TimeFilter
|
||||
): Promise<Array<{ type: MessageType; count: number }>> {
|
||||
return window.chatApi.getMessageTypeDistribution(sessionId, filter)
|
||||
}
|
||||
|
||||
// ==================== 成员管理 ====================
|
||||
|
||||
getMembers(sessionId: string): Promise<MemberWithStats[]> {
|
||||
return window.chatApi.getMembers(sessionId)
|
||||
}
|
||||
|
||||
getMembersPaginated(sessionId: string, params: PaginationParams): Promise<PaginatedResult<MemberWithStats>> {
|
||||
return window.chatApi.getMembersPaginated(sessionId, params).then((result) => ({
|
||||
items: result.members,
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
totalPages: result.totalPages,
|
||||
}))
|
||||
}
|
||||
|
||||
getMemberNameHistory(sessionId: string, memberId: number): Promise<MemberNameHistory[]> {
|
||||
return window.chatApi.getMemberNameHistory(sessionId, memberId)
|
||||
}
|
||||
|
||||
updateMemberAliases(sessionId: string, memberId: number, aliases: string[]): Promise<boolean> {
|
||||
return window.chatApi.updateMemberAliases(sessionId, memberId, aliases)
|
||||
}
|
||||
|
||||
mergeMembers(sessionId: string, memberId1: number, memberId2: number): Promise<boolean> {
|
||||
return window.chatApi.mergeMembers(sessionId, memberId1, memberId2)
|
||||
}
|
||||
|
||||
deleteMember(sessionId: string, memberId: number): Promise<boolean> {
|
||||
return window.chatApi.deleteMember(sessionId, memberId)
|
||||
}
|
||||
|
||||
// ==================== 社交分析 ====================
|
||||
|
||||
getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter): Promise<CatchphraseAnalysis> {
|
||||
return window.chatApi.getCatchphraseAnalysis(sessionId, filter)
|
||||
}
|
||||
|
||||
getLanguagePreferenceAnalysis(
|
||||
sessionId: string,
|
||||
locale: string,
|
||||
filter?: TimeFilter,
|
||||
dictType?: string
|
||||
): Promise<LanguagePreferenceResult> {
|
||||
return window.chatApi.getLanguagePreferenceAnalysis(sessionId, locale, filter, dictType)
|
||||
}
|
||||
|
||||
getMentionAnalysis(sessionId: string, filter?: TimeFilter): Promise<MentionAnalysis> {
|
||||
return window.chatApi.getMentionAnalysis(sessionId, filter)
|
||||
}
|
||||
|
||||
getMentionGraph(sessionId: string, filter?: TimeFilter): Promise<MentionGraphData> {
|
||||
return window.chatApi.getMentionGraph(sessionId, filter)
|
||||
}
|
||||
|
||||
getClusterGraph(sessionId: string, filter?: TimeFilter, options?: ClusterGraphOptions): Promise<ClusterGraphData> {
|
||||
return window.chatApi.getClusterGraph(sessionId, filter, options)
|
||||
}
|
||||
|
||||
getLaughAnalysis(sessionId: string, filter?: TimeFilter, keywords?: string[]): Promise<LaughAnalysis> {
|
||||
return window.chatApi.getLaughAnalysis(sessionId, filter, keywords)
|
||||
}
|
||||
|
||||
getRelationshipStats(
|
||||
sessionId: string,
|
||||
filter?: TimeFilter,
|
||||
options?: { perseveranceThreshold?: number }
|
||||
): Promise<RelationshipStats> {
|
||||
return window.chatApi.getRelationshipStats(sessionId, filter, options)
|
||||
}
|
||||
|
||||
// ==================== SQL Lab ====================
|
||||
|
||||
executeSQL(sessionId: string, sql: string): Promise<SQLResult> {
|
||||
return window.chatApi.executeSQL(sessionId, sql)
|
||||
}
|
||||
|
||||
getSchema(sessionId: string): Promise<TableSchema[]> {
|
||||
return window.chatApi.getSchema(sessionId)
|
||||
}
|
||||
|
||||
// ==================== 插件系统 ====================
|
||||
|
||||
pluginQuery<T = Record<string, unknown>>(sessionId: string, sql: string, params?: unknown[]): Promise<T[]> {
|
||||
return window.chatApi.pluginQuery<T>(sessionId, sql, params)
|
||||
}
|
||||
|
||||
pluginCompute<T = unknown>(fnString: string, input: unknown): Promise<T> {
|
||||
return window.chatApi.pluginCompute<T>(fnString, input)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 适配器工厂
|
||||
*
|
||||
* 根据编译时常量选择正确的 QueryAdapter 实现:
|
||||
* - Electron: ElectronAdapter (window.chatApi IPC)
|
||||
* - CLI serve Web: FetchAdapter (/_web/ HTTP API)
|
||||
* - 在线版 Web: BrowserSqlAdapter (sql.js Web Worker)
|
||||
*
|
||||
* 使用 initAdapter() 初始化(异步,动态 import 按需加载),
|
||||
* 之后通过 getAdapter() 同步获取。
|
||||
*/
|
||||
|
||||
import { IS_ELECTRON, IS_BROWSER_STANDALONE } from '@/utils/platform'
|
||||
import type { QueryAdapter } from './types'
|
||||
|
||||
let _adapter: QueryAdapter | null = null
|
||||
|
||||
/**
|
||||
* 初始化适配器(应在 app 启动时调用一次)
|
||||
*/
|
||||
export async function initAdapter(): Promise<QueryAdapter> {
|
||||
if (_adapter) return _adapter
|
||||
|
||||
if (IS_ELECTRON) {
|
||||
const { ElectronAdapter } = await import('./electron')
|
||||
_adapter = new ElectronAdapter()
|
||||
} else if (IS_BROWSER_STANDALONE) {
|
||||
// Phase 3B: BrowserSqlAdapter
|
||||
throw new Error('BrowserSqlAdapter is not yet implemented.')
|
||||
} else {
|
||||
// Phase 3A: FetchAdapter
|
||||
throw new Error('FetchAdapter is not yet implemented.')
|
||||
}
|
||||
|
||||
return _adapter!
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步获取已初始化的适配器(initAdapter 之后可用)
|
||||
*/
|
||||
export function getAdapter(): QueryAdapter {
|
||||
if (!_adapter) {
|
||||
throw new Error('Adapter not initialized. Call initAdapter() first.')
|
||||
}
|
||||
return _adapter
|
||||
}
|
||||
|
||||
export type { QueryAdapter, AdapterCapabilities } from './types'
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* QueryAdapter — 统一的数据访问适配器接口
|
||||
*
|
||||
* 三种实现:
|
||||
* - ElectronAdapter: 包装 window.chatApi (IPC)
|
||||
* - FetchAdapter: 调用 /_web/ 内部 HTTP API
|
||||
* - BrowserSqlAdapter: sql.js Web Worker (纯浏览器)
|
||||
*
|
||||
* 所有方法以 sessionId 为参数,适配器内部负责打开/管理数据库连接。
|
||||
* 返回类型复用 src/types/ 中已有的前端类型,避免 UI 层改动。
|
||||
*/
|
||||
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
import type {
|
||||
MemberActivity,
|
||||
MemberWithStats,
|
||||
MemberNameHistory,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
WeekdayActivity,
|
||||
MonthlyActivity,
|
||||
CatchphraseAnalysis,
|
||||
MentionAnalysis,
|
||||
LaughAnalysis,
|
||||
ClusterGraphData,
|
||||
ClusterGraphOptions,
|
||||
RelationshipStats,
|
||||
} from '@/types/analysis'
|
||||
import type { LanguagePreferenceResult } from '@/types/quotes/languagePreference'
|
||||
|
||||
// ==================== 能力声明 ====================
|
||||
|
||||
export interface AdapterCapabilities {
|
||||
/** 支持 AI 对话 / 分析 */
|
||||
ai: boolean
|
||||
/** 支持中文分词 / 词云 */
|
||||
nlp: boolean
|
||||
/** 支持 FTS5 全文搜索(否则降级为 LIKE) */
|
||||
fts: boolean
|
||||
/** 支持原生文件对话框 */
|
||||
nativeFileDialog: boolean
|
||||
/** 支持剪贴板 / 系统托盘等 OS 集成 */
|
||||
osIntegration: boolean
|
||||
/** 支持 Demo 数据导入 */
|
||||
demoImport: boolean
|
||||
/** 支持插件系统 (pluginQuery / pluginCompute) */
|
||||
plugin: boolean
|
||||
/** 支持数据库迁移 */
|
||||
migration: boolean
|
||||
/** 支持会话合并 */
|
||||
merge: boolean
|
||||
/** 支持增量导入 */
|
||||
incrementalImport: boolean
|
||||
}
|
||||
|
||||
// ==================== 分页参数与结果 ====================
|
||||
|
||||
export interface PaginationParams {
|
||||
page: number
|
||||
pageSize: number
|
||||
search?: string
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
// ==================== SQL Lab 结果 ====================
|
||||
|
||||
export interface SQLResult {
|
||||
columns: string[]
|
||||
rows: unknown[][]
|
||||
rowCount: number
|
||||
duration: number
|
||||
limited: boolean
|
||||
}
|
||||
|
||||
export interface TableSchema {
|
||||
name: string
|
||||
columns: Array<{
|
||||
name: string
|
||||
type: string
|
||||
notnull: boolean
|
||||
pk: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
// ==================== Mention Graph ====================
|
||||
|
||||
export interface MentionGraphData {
|
||||
nodes: Array<{ id: number; name: string; value: number; symbolSize: number }>
|
||||
links: Array<{ source: string; target: string; value: number }>
|
||||
maxLinkValue: number
|
||||
}
|
||||
|
||||
// ==================== 消息长度分布 ====================
|
||||
|
||||
export interface MessageLengthDistribution {
|
||||
detail: Array<{ len: number; count: number }>
|
||||
grouped: Array<{ range: string; count: number }>
|
||||
}
|
||||
|
||||
// ==================== 核心适配器接口 ====================
|
||||
|
||||
export interface QueryAdapter {
|
||||
/** 返回当前适配器的能力声明 */
|
||||
getCapabilities(): AdapterCapabilities
|
||||
|
||||
// ==================== 会话管理 ====================
|
||||
|
||||
getSessions(): Promise<AnalysisSession[]>
|
||||
getSession(sessionId: string): Promise<AnalysisSession | null>
|
||||
deleteSession(sessionId: string): Promise<boolean>
|
||||
renameSession(sessionId: string, newName: string): Promise<boolean>
|
||||
updateSessionOwnerId(sessionId: string, ownerId: string | null): Promise<boolean>
|
||||
|
||||
// ==================== 时间范围 ====================
|
||||
|
||||
getAvailableYears(sessionId: string): Promise<number[]>
|
||||
getTimeRange(sessionId: string): Promise<{ start: number; end: number } | null>
|
||||
|
||||
// ==================== 统计分析 ====================
|
||||
|
||||
getMemberActivity(sessionId: string, filter?: TimeFilter): Promise<MemberActivity[]>
|
||||
getHourlyActivity(sessionId: string, filter?: TimeFilter): Promise<HourlyActivity[]>
|
||||
getDailyActivity(sessionId: string, filter?: TimeFilter): Promise<DailyActivity[]>
|
||||
getWeekdayActivity(sessionId: string, filter?: TimeFilter): Promise<WeekdayActivity[]>
|
||||
getMonthlyActivity(sessionId: string, filter?: TimeFilter): Promise<MonthlyActivity[]>
|
||||
getYearlyActivity(sessionId: string, filter?: TimeFilter): Promise<Array<{ year: number; messageCount: number }>>
|
||||
getMessageLengthDistribution(sessionId: string, filter?: TimeFilter): Promise<MessageLengthDistribution>
|
||||
getMessageTypeDistribution(
|
||||
sessionId: string,
|
||||
filter?: TimeFilter
|
||||
): Promise<Array<{ type: MessageType; count: number }>>
|
||||
|
||||
// ==================== 成员管理 ====================
|
||||
|
||||
getMembers(sessionId: string): Promise<MemberWithStats[]>
|
||||
getMembersPaginated(sessionId: string, params: PaginationParams): Promise<PaginatedResult<MemberWithStats>>
|
||||
getMemberNameHistory(sessionId: string, memberId: number): Promise<MemberNameHistory[]>
|
||||
updateMemberAliases(sessionId: string, memberId: number, aliases: string[]): Promise<boolean>
|
||||
mergeMembers(sessionId: string, memberId1: number, memberId2: number): Promise<boolean>
|
||||
deleteMember(sessionId: string, memberId: number): Promise<boolean>
|
||||
|
||||
// ==================== 社交分析 ====================
|
||||
|
||||
getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter): Promise<CatchphraseAnalysis>
|
||||
getLanguagePreferenceAnalysis(
|
||||
sessionId: string,
|
||||
locale: string,
|
||||
filter?: TimeFilter,
|
||||
dictType?: string
|
||||
): Promise<LanguagePreferenceResult>
|
||||
getMentionAnalysis(sessionId: string, filter?: TimeFilter): Promise<MentionAnalysis>
|
||||
getMentionGraph(sessionId: string, filter?: TimeFilter): Promise<MentionGraphData>
|
||||
getClusterGraph(sessionId: string, filter?: TimeFilter, options?: ClusterGraphOptions): Promise<ClusterGraphData>
|
||||
getLaughAnalysis(sessionId: string, filter?: TimeFilter, keywords?: string[]): Promise<LaughAnalysis>
|
||||
getRelationshipStats(
|
||||
sessionId: string,
|
||||
filter?: TimeFilter,
|
||||
options?: { perseveranceThreshold?: number }
|
||||
): Promise<RelationshipStats>
|
||||
|
||||
// ==================== SQL Lab ====================
|
||||
|
||||
executeSQL(sessionId: string, sql: string): Promise<SQLResult>
|
||||
getSchema(sessionId: string): Promise<TableSchema[]>
|
||||
|
||||
// ==================== 插件系统(能力依赖) ====================
|
||||
|
||||
pluginQuery<T = Record<string, unknown>>(sessionId: string, sql: string, params?: unknown[]): Promise<T[]>
|
||||
pluginCompute<T = unknown>(fnString: string, input: unknown): Promise<T>
|
||||
}
|
||||
Vendored
+1
@@ -1,6 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __IS_ELECTRON__: boolean | undefined
|
||||
declare const __IS_BROWSER_STANDALONE__: boolean | undefined
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
/**
|
||||
* 平台检测工具
|
||||
*
|
||||
* 通过编译时注入的 __IS_ELECTRON__ 常量区分运行环境。
|
||||
* Electron renderer 构建时注入 true,未来 Web 版构建时不定义或为 false。
|
||||
* 通过编译时注入的常量区分运行环境:
|
||||
* - Electron: __IS_ELECTRON__ = true
|
||||
* - CLI serve Web: 两者都为 false(默认,FetchAdapter)
|
||||
* - 在线版 Web: __IS_BROWSER_STANDALONE__ = true(BrowserSqlAdapter)
|
||||
*/
|
||||
|
||||
declare const __IS_ELECTRON__: boolean | undefined
|
||||
declare const __IS_BROWSER_STANDALONE__: boolean | undefined
|
||||
|
||||
export const IS_ELECTRON = typeof __IS_ELECTRON__ !== 'undefined' && __IS_ELECTRON__
|
||||
|
||||
export const IS_BROWSER_STANDALONE = typeof __IS_BROWSER_STANDALONE__ !== 'undefined' && __IS_BROWSER_STANDALONE__
|
||||
Reference in new issue
Block a user