feat(merge): migrate merge routes from IPC to shared HTTP package

Unify the merge workflow (parse/conflicts/execute/clear/export-for-merge)
across CLI, Electron, and web-serve into a single set of shared routes in
@openchatlab/http-routes.

Key changes:
- Move MergeSessionCache from CLI to node-runtime shared package
- Add dual-mode parse route (multipart upload for web, JSON filePath for Electron)
- Integrate merge routes into Electron internal API server with @fastify/multipart
- Create installMergeShims() with filePath-to-handle mapping for frontend
- Remove Electron merge IPC handlers, merger module, and preload bridges (~790 lines deleted)
- Address CodeX review: CHAT_DB_TABLES in openDatabase, no chatApi patching,
  outputName sanitization, resp.ok checks, @fastify/multipart in desktop deps

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
diguaandCursor committed 2026-06-02 15:41:21 +08:00
1 parent 65fb5179cd
commit ae90db63a5
22 files changed
+432 -790

No files matched your search

+1 -1
View File
@@ -332,7 +332,7 @@ export * as memberService from './services/member-service'
export * as sessionIndexService from './services/session-index-service'
export * as summaryService from './services/summary-service'
export * as exportService from './services/export-service'
export { createDatabaseManagerAdapter } from './services'
export { createDatabaseManagerAdapter, MergeSessionCache } from './services'
export type {
SessionRuntimeAdapter,
AnalysisSessionDTO,
@@ -41,3 +41,6 @@ export type { LlmConfig, SummaryServiceDeps } from './summary-service'
// Export service
export { exportMarkdown } from './export-service'
// Merge cache
export { MergeSessionCache } from './merge-cache'
@@ -0,0 +1,99 @@
/**
* Server-side merge session cache.
*
* Manages temporary databases for the multi-file merge workflow.
* Each uploaded/parsed file gets a temp DB handle that persists
* across HTTP requests until merge completes or cache is cleared.
*/
import * as fs from 'fs'
import * as path from 'path'
import * as crypto from 'crypto'
import type { PathProvider, DatabaseAdapter } from '@openchatlab/core'
import { openBetterSqliteDatabase } from '../better-sqlite3-adapter'
import { TempDbReader, deleteTempDatabase, cleanupTempDatabases } from '../merger/temp-db'
interface CacheEntry {
tempDbPath: string
filename: string
createdAt: number
}
export class MergeSessionCache {
private cache = new Map<string, CacheEntry>()
private tempDir: string
private nativeBinding?: string
constructor(pathProvider: PathProvider, options?: { nativeBinding?: string }) {
this.tempDir = pathProvider.getTempDir()
this.nativeBinding = options?.nativeBinding
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
}
}
generateTempDbPath(filename: string): string {
const ts = Date.now()
const rand = Math.random().toString(36).slice(2, 8)
const safeName = filename.replace(/[/\\?%*:|"<>]/g, '_').slice(0, 40)
return path.join(this.tempDir, `merge_${safeName}_${ts}_${rand}.db`)
}
/** Store a parsed file's temp DB and return a handle for subsequent requests. */
store(filename: string, tempDbPath: string): string {
const handle = crypto.randomUUID()
this.cache.set(handle, { tempDbPath, filename, createdAt: Date.now() })
return handle
}
/** Open a TempDbReader for a given handle. Caller must close the reader. */
openReader(handle: string): { reader: TempDbReader; filename: string } | null {
const entry = this.cache.get(handle)
if (!entry || !fs.existsSync(entry.tempDbPath)) return null
const adapter = openBetterSqliteDatabase(entry.tempDbPath, {
readonly: true,
nativeBinding: this.nativeBinding,
})
return { reader: new TempDbReader(adapter), filename: entry.filename }
}
/** Create a writable temp DB for streaming parse. */
createTempDatabase(filename: string): { db: DatabaseAdapter; tempDbPath: string } {
const tempDbPath = this.generateTempDbPath(filename)
const adapter = openBetterSqliteDatabase(tempDbPath, { nativeBinding: this.nativeBinding })
return { db: adapter, tempDbPath }
}
/** Remove a single entry and its temp DB file. */
delete(handle: string): void {
const entry = this.cache.get(handle)
if (entry) {
deleteTempDatabase(entry.tempDbPath)
this.cache.delete(handle)
}
}
/** Clear all entries and delete their temp DB files. */
clear(): void {
for (const entry of this.cache.values()) {
deleteTempDatabase(entry.tempDbPath)
}
this.cache.clear()
}
/** Cleanup orphan temp DBs from previous runs. */
cleanupOrphans(): void {
cleanupTempDatabases(this.tempDir)
}
/** Auto-cleanup entries older than maxAge (default 1 hour). */
cleanupExpired(maxAgeMs = 3600_000): void {
const now = Date.now()
for (const [handle, entry] of this.cache) {
if (now - entry.createdAt > maxAgeMs) {
deleteTempDatabase(entry.tempDbPath)
this.cache.delete(handle)
}
}
}
}