mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
Move Electron-specific code (main, preload, shared) and configs (electron.vite, electron-builder) into apps/desktop/ to prepare for multi-platform support (Web Browser standalone). - Create apps/desktop/ with dedicated package.json and tsconfig - Split Electron deps (electron, electron-updater, better-sqlite3, etc.) from shared frontend deps in root package.json - Declare better-sqlite3 and @node-rs/jieba in packages/node-runtime - Add stream-json to packages/parser where it belongs - Update all path aliases (@electron/* → apps/desktop/*) in tsconfig, vite configs, and relative imports in preload/main - Keep slim root tsconfig.node.json as shared base for packages - Update CI/CD release workflow for new directory layout - Fix pre-existing type errors (pi-ai Model generic, SQLLabTab LLMChatStreamChunk, filter.test.ts empty methods)
243 lines
6.4 KiB
TypeScript
243 lines
6.4 KiB
TypeScript
/**
|
|
* ChatLab API — IPC handlers for renderer process (hierarchical data source model)
|
|
*
|
|
* Migrated to @openchatlab/sync shared package.
|
|
*/
|
|
|
|
import { ipcMain, net } from 'electron'
|
|
import type { IpcContext } from './types'
|
|
import * as apiServer from '../api'
|
|
import { setConfigManager } from '../api'
|
|
import { getSettingsDir } from '../paths'
|
|
import { apiLogger } from '../api/logger'
|
|
import { getImportingStatus } from '../api/routes/import'
|
|
import { ElectronFetcher, WorkerImporter, BrowserWindowNotifier } from '../api/adapters'
|
|
import {
|
|
ConfigManager,
|
|
DataSourceManager,
|
|
PullEngine,
|
|
initScheduler,
|
|
stopAllTimers,
|
|
stopTimer,
|
|
reloadTimer,
|
|
buildRemoteSessionsUrl,
|
|
parseRemoteSessionsResponse,
|
|
normalizeBaseUrl,
|
|
} from '@openchatlab/sync'
|
|
import type { DataSourceUpdatable, RemoteSessionDiscoveryQuery, RemoteSessionDiscoveryResult } from '@openchatlab/sync'
|
|
|
|
const syncLogger = {
|
|
info: (msg: string) => apiLogger.info(msg),
|
|
warn: (msg: string) => apiLogger.warn(msg),
|
|
error: (msg: string, err?: unknown) => apiLogger.error(msg, err),
|
|
}
|
|
|
|
let configManager: ConfigManager
|
|
let dsManager: DataSourceManager
|
|
let pullEngine: PullEngine
|
|
|
|
function ensureInstances(): void {
|
|
if (configManager) return
|
|
|
|
const settingsDir = getSettingsDir()
|
|
configManager = new ConfigManager(settingsDir, syncLogger)
|
|
dsManager = new DataSourceManager(settingsDir, syncLogger)
|
|
setConfigManager(configManager)
|
|
|
|
pullEngine = new PullEngine({
|
|
fetcher: new ElectronFetcher(),
|
|
importer: new WorkerImporter(syncLogger),
|
|
notifier: new BrowserWindowNotifier(),
|
|
dsManager,
|
|
logger: syncLogger,
|
|
isImporting: getImportingStatus,
|
|
})
|
|
}
|
|
|
|
/** Exported for use by api/index.ts */
|
|
export function getConfigManager(): ConfigManager {
|
|
ensureInstances()
|
|
return configManager
|
|
}
|
|
|
|
function fetchRemoteSessions(
|
|
baseUrl: string,
|
|
token?: string,
|
|
query: RemoteSessionDiscoveryQuery = {}
|
|
): Promise<RemoteSessionDiscoveryResult> {
|
|
return new Promise<RemoteSessionDiscoveryResult>((resolve, reject) => {
|
|
const url = buildRemoteSessionsUrl(normalizeBaseUrl(baseUrl), query)
|
|
|
|
const request = net.request(url)
|
|
if (token) {
|
|
request.setHeader('Authorization', `Bearer ${token}`)
|
|
}
|
|
request.setHeader('Accept', 'application/json')
|
|
|
|
let body = ''
|
|
|
|
request.on('response', (response) => {
|
|
if (response.statusCode !== 200) {
|
|
reject(new Error(`Remote server returned HTTP ${response.statusCode}`))
|
|
return
|
|
}
|
|
|
|
response.on('data', (chunk: Buffer) => {
|
|
body += chunk.toString('utf-8')
|
|
})
|
|
|
|
response.on('end', () => {
|
|
try {
|
|
resolve(parseRemoteSessionsResponse(body))
|
|
} catch {
|
|
reject(new Error('Failed to parse remote sessions response'))
|
|
}
|
|
})
|
|
|
|
response.on('error', (err: Error) => {
|
|
reject(err)
|
|
})
|
|
})
|
|
|
|
request.on('error', (err: Error) => {
|
|
reject(err)
|
|
})
|
|
|
|
request.end()
|
|
})
|
|
}
|
|
|
|
export function registerApiHandlers(_ctx: IpcContext): void {
|
|
ensureInstances()
|
|
|
|
// ==================== API Server Management ====================
|
|
|
|
ipcMain.handle('api:getConfig', () => {
|
|
const config = configManager.load()
|
|
return {
|
|
enabled: config.enabled,
|
|
port: config.port,
|
|
token: config.token,
|
|
createdAt: config.createdAt,
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('api:getStatus', () => {
|
|
return apiServer.getStatus()
|
|
})
|
|
|
|
ipcMain.handle('api:setEnabled', async (_event, enabled: boolean) => {
|
|
return apiServer.setEnabled(enabled)
|
|
})
|
|
|
|
ipcMain.handle('api:setPort', async (_event, port: number) => {
|
|
return apiServer.setPort(port)
|
|
})
|
|
|
|
ipcMain.handle('api:regenerateToken', () => {
|
|
return configManager.regenerateToken()
|
|
})
|
|
|
|
ipcMain.handle('api:updateConfig', (_event, partial: Record<string, unknown>) => {
|
|
return configManager.update(partial as any)
|
|
})
|
|
|
|
// ==================== Data Source Management ====================
|
|
|
|
ipcMain.handle('api:getDataSources', () => {
|
|
return dsManager.loadAll()
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'api:addDataSource',
|
|
(
|
|
_event,
|
|
partial: { name?: string; baseUrl: string; token: string; intervalMinutes: number; pullLimit?: number }
|
|
) => {
|
|
return dsManager.add(partial)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('api:updateDataSource', (_event, id: string, updates: DataSourceUpdatable) => {
|
|
const ds = dsManager.update(id, updates)
|
|
if (ds) {
|
|
reloadTimer(ds.id)
|
|
}
|
|
return ds
|
|
})
|
|
|
|
ipcMain.handle('api:deleteDataSource', (_event, id: string) => {
|
|
stopTimer(id)
|
|
return dsManager.delete(id)
|
|
})
|
|
|
|
// ==================== Import Session Management ====================
|
|
|
|
ipcMain.handle(
|
|
'api:addImportSessions',
|
|
(_event, sourceId: string, sessions: Array<{ name: string; remoteSessionId: string }>) => {
|
|
const added = dsManager.addSessions(sourceId, sessions)
|
|
reloadTimer(sourceId, true)
|
|
return added
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('api:removeImportSession', (_event, sourceId: string, sessionId: string) => {
|
|
const result = dsManager.removeSession(sourceId, sessionId)
|
|
reloadTimer(sourceId)
|
|
return result
|
|
})
|
|
|
|
// ==================== Sync ====================
|
|
|
|
ipcMain.handle('api:triggerPull', async (_event, sourceId: string, sessionId?: string) => {
|
|
return pullEngine.triggerPull(sourceId, sessionId)
|
|
})
|
|
|
|
ipcMain.handle('api:triggerPullAll', async (_event, sourceId: string) => {
|
|
return pullEngine.triggerPullAll(sourceId)
|
|
})
|
|
|
|
// ==================== Remote Discovery ====================
|
|
|
|
ipcMain.handle(
|
|
'api:fetchRemoteSessions',
|
|
async (_event, baseUrl: string, token: string, query?: { keyword?: string; limit?: number; cursor?: string }) => {
|
|
try {
|
|
return await fetchRemoteSessions(baseUrl, token || undefined, query)
|
|
} catch (err: any) {
|
|
throw new Error(err.message || 'Failed to fetch remote sessions')
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Auto-start API server and Pull scheduler after app launch
|
|
*/
|
|
export async function initApiServer(ctx: IpcContext): Promise<void> {
|
|
ensureInstances()
|
|
|
|
await apiServer.autoStart()
|
|
|
|
const status = apiServer.getStatus()
|
|
if (status.error) {
|
|
ctx.win.webContents.once('did-finish-load', () => {
|
|
ctx.win.webContents.send('api:startupError', {
|
|
error: status.error,
|
|
})
|
|
})
|
|
}
|
|
|
|
initScheduler({
|
|
dsManager,
|
|
pullEngine,
|
|
logger: syncLogger,
|
|
})
|
|
}
|
|
|
|
export async function cleanupApiServer(): Promise<void> {
|
|
stopAllTimers()
|
|
await apiServer.stop()
|
|
}
|