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:
digua committed 2026-05-13 19:37:57 +08:00
1 parent 847d75e4b6
commit f6b3dabc31
5 files changed
+447 -2

No files matched your search

+212
View File
@@ -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)
}
}
+48
View File
@@ -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'
+179
View File
@@ -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>
}
+1
View File
@@ -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'
+7 -2
View File
@@ -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__