570 lines
16 KiB
TypeScript
570 lines
16 KiB
TypeScript
/**
|
||
* IPC 通信处理器
|
||
* 安全地暴露主进程功能给渲染进程
|
||
*/
|
||
import { ipcMain, app, BrowserWindow, dialog } from 'electron'
|
||
import { promises as fs } from 'fs'
|
||
import * as fsSync from 'fs'
|
||
import { exec } from 'child_process'
|
||
import util from 'util'
|
||
import path from 'path'
|
||
import { getSettings, saveSettings } from './store'
|
||
import { cacheManager } from './cache-manager'
|
||
|
||
// IPC 通道名称常量
|
||
export const IPC_CHANNELS = {
|
||
// 文件操作
|
||
SAVE_FILE: 'file:save',
|
||
READ_FILE: 'file:read',
|
||
// 设置操作
|
||
GET_SETTINGS: 'settings:get',
|
||
SAVE_SETTINGS: 'settings:save',
|
||
// 缓存操作
|
||
CACHE_SET_JSON: 'cache:setJSON',
|
||
CACHE_GET_JSON: 'cache:getJSON',
|
||
CACHE_SET_BINARY: 'cache:setBinary',
|
||
CACHE_GET_BINARY: 'cache:getBinary',
|
||
CACHE_HAS: 'cache:has',
|
||
CACHE_DELETE: 'cache:delete',
|
||
CACHE_CLEAR_NAMESPACE: 'cache:clearNamespace',
|
||
CACHE_CLEAR_ALL: 'cache:clearAll',
|
||
CACHE_GET_STATS: 'cache:getStats',
|
||
// 窗口操作
|
||
WINDOW_MINIMIZE: 'window:minimize',
|
||
WINDOW_MAXIMIZE: 'window:maximize',
|
||
WINDOW_CLOSE: 'window:close',
|
||
// Git/System 操作
|
||
DIALOG_OPEN_DIRECTORY: 'dialog:openDirectory',
|
||
GIT_CLONE: 'git:clone'
|
||
} as const
|
||
|
||
/**
|
||
* 设置所有 IPC 处理器
|
||
*/
|
||
export function setupIpcHandlers() {
|
||
// ============================================
|
||
// 文件操作相关 IPC
|
||
// ============================================
|
||
|
||
/**
|
||
* 保存文件到桌面
|
||
* @param content - 要保存的文件内容
|
||
* @param filename - 文件名(默认为 demo-note.txt)
|
||
* @returns 保存结果
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.SAVE_FILE,
|
||
async (_event, content: string, filename: string = 'demo-note.txt') => {
|
||
try {
|
||
// 获取桌面路径
|
||
const desktopPath = app.getPath('desktop')
|
||
const filePath = path.join(desktopPath, filename)
|
||
|
||
// 写入文件(使用 UTF-8 编码)
|
||
await fs.writeFile(filePath, content, 'utf-8')
|
||
|
||
return {
|
||
success: true,
|
||
message: `文件已保存到: ${filePath}`,
|
||
path: filePath
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('保存文件失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
message: `保存失败: ${errorMessage}`,
|
||
path: null
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 从桌面读取文件
|
||
* @param filename - 文件名(默认为 demo-note.txt)
|
||
* @returns 文件内容
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.READ_FILE,
|
||
async (_event, filename: string = 'demo-note.txt') => {
|
||
try {
|
||
// 获取桌面路径
|
||
const desktopPath = app.getPath('desktop')
|
||
const filePath = path.join(desktopPath, filename)
|
||
|
||
// 检查文件是否存在
|
||
try {
|
||
await fs.access(filePath)
|
||
} catch {
|
||
return {
|
||
success: false,
|
||
message: '文件不存在,请先保存一个文件',
|
||
content: null
|
||
}
|
||
}
|
||
|
||
// 读取文件内容
|
||
const content = await fs.readFile(filePath, 'utf-8')
|
||
|
||
return {
|
||
success: true,
|
||
message: '文件读取成功',
|
||
content
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('读取文件失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
message: `读取失败: ${errorMessage}`,
|
||
content: null
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
// ============================================
|
||
// 用户设置相关 IPC
|
||
// ============================================
|
||
|
||
/**
|
||
* 获取用户设置
|
||
* @param key - 可选的设置键名,不传则返回所有设置
|
||
* @returns 设置值
|
||
*/
|
||
ipcMain.handle(IPC_CHANNELS.GET_SETTINGS, async (_event, key?: string) => {
|
||
try {
|
||
const settings = getSettings(key)
|
||
return {
|
||
success: true,
|
||
data: settings
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('获取设置失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
data: null,
|
||
message: errorMessage
|
||
}
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 保存用户设置
|
||
* @param key - 设置键名
|
||
* @param value - 设置值
|
||
* @returns 保存结果
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.SAVE_SETTINGS,
|
||
async (_event, key: string, value: unknown) => {
|
||
try {
|
||
saveSettings(key, value)
|
||
return {
|
||
success: true,
|
||
message: '设置已保存'
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('保存设置失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
message: `保存失败: ${errorMessage}`
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
// ============================================
|
||
// 缓存操作相关 IPC
|
||
// ============================================
|
||
|
||
/**
|
||
* 设置JSON缓存
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_SET_JSON,
|
||
async (_event, key: string, data: any, config?: { maxAge?: number; namespace?: string }) => {
|
||
try {
|
||
await cacheManager.setJSON(key, data, config)
|
||
return {
|
||
success: true,
|
||
message: '缓存已设置'
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('设置JSON缓存失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
message: `设置失败: ${errorMessage}`
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 获取JSON缓存
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_GET_JSON,
|
||
async (_event, key: string, config?: { maxAge?: number; namespace?: string }) => {
|
||
try {
|
||
const data = await cacheManager.getJSON(key, config)
|
||
return {
|
||
success: true,
|
||
data
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('获取JSON缓存失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
data: null,
|
||
message: errorMessage
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 设置二进制缓存
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_SET_BINARY,
|
||
async (_event, key: string, buffer: Buffer, type: 'binary' | 'image' = 'binary', config?: { maxAge?: number; namespace?: string }) => {
|
||
try {
|
||
await cacheManager.setBinary(key, buffer, type, config)
|
||
return {
|
||
success: true,
|
||
message: '二进制缓存已设置'
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('设置二进制缓存失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
message: `设置失败: ${errorMessage}`
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 获取二进制缓存
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_GET_BINARY,
|
||
async (_event, key: string, config?: { maxAge?: number; namespace?: string }) => {
|
||
try {
|
||
const buffer = await cacheManager.getBinary(key, config)
|
||
return {
|
||
success: true,
|
||
data: buffer
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('获取二进制缓存失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
data: null,
|
||
message: errorMessage
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 检查缓存是否存在
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_HAS,
|
||
async (_event, key: string, config?: { namespace?: string }) => {
|
||
try {
|
||
const exists = await cacheManager.has(key, config)
|
||
return {
|
||
success: true,
|
||
exists
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('检查缓存失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
exists: false,
|
||
message: errorMessage
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 删除单个缓存
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_DELETE,
|
||
async (_event, key: string, namespace?: string) => {
|
||
try {
|
||
await cacheManager.delete(key, namespace)
|
||
return {
|
||
success: true,
|
||
message: '缓存已删除'
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('删除缓存失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
message: `删除失败: ${errorMessage}`
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 清空命名空间缓存
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_CLEAR_NAMESPACE,
|
||
async (_event, namespace: string) => {
|
||
try {
|
||
await cacheManager.clearNamespace(namespace)
|
||
return {
|
||
success: true,
|
||
message: `命名空间 ${namespace} 的缓存已清空`
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('清空命名空间缓存失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
message: `清空失败: ${errorMessage}`
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 清空所有缓存
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_CLEAR_ALL,
|
||
async (_event) => {
|
||
try {
|
||
await cacheManager.clearAll()
|
||
return {
|
||
success: true,
|
||
message: '所有缓存已清空'
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('清空所有缓存失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
message: `清空失败: ${errorMessage}`
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 获取缓存统计信息
|
||
*/
|
||
ipcMain.handle(
|
||
IPC_CHANNELS.CACHE_GET_STATS,
|
||
async (_event) => {
|
||
try {
|
||
const stats = await cacheManager.getStats()
|
||
return {
|
||
success: true,
|
||
data: stats
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('获取缓存统计信息失败:', errorMessage)
|
||
return {
|
||
success: false,
|
||
data: null,
|
||
message: errorMessage
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
// ============================================
|
||
// 窗口操作相关 IPC
|
||
// ============================================
|
||
|
||
/**
|
||
* 最小化窗口
|
||
*/
|
||
ipcMain.handle(IPC_CHANNELS.WINDOW_MINIMIZE, async (event) => {
|
||
try {
|
||
const window = BrowserWindow.fromWebContents(event.sender)
|
||
if (window) {
|
||
window.minimize()
|
||
return { success: true }
|
||
}
|
||
return { success: false, message: '无法找到窗口' }
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('最小化窗口失败:', errorMessage)
|
||
return { success: false, message: errorMessage }
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 最大化/还原窗口
|
||
*/
|
||
ipcMain.handle(IPC_CHANNELS.WINDOW_MAXIMIZE, async (event) => {
|
||
try {
|
||
const window = BrowserWindow.fromWebContents(event.sender)
|
||
if (window) {
|
||
if (window.isMaximized()) {
|
||
window.unmaximize()
|
||
} else {
|
||
window.maximize()
|
||
}
|
||
return { success: true, isMaximized: window.isMaximized() }
|
||
}
|
||
return { success: false, message: '无法找到窗口' }
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('最大化窗口失败:', errorMessage)
|
||
return { success: false, message: errorMessage }
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 关闭窗口
|
||
*/
|
||
ipcMain.handle(IPC_CHANNELS.WINDOW_CLOSE, async (event) => {
|
||
try {
|
||
const window = BrowserWindow.fromWebContents(event.sender)
|
||
if (window) {
|
||
window.close()
|
||
return { success: true }
|
||
}
|
||
return { success: false, message: '无法找到窗口' }
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('关闭窗口失败:', errorMessage)
|
||
return { success: false, message: errorMessage }
|
||
}
|
||
})
|
||
|
||
// ============================================
|
||
// Git/System 操作相关 IPC
|
||
// ============================================
|
||
|
||
/**
|
||
* 打开目录选择对话框
|
||
*/
|
||
ipcMain.handle(IPC_CHANNELS.DIALOG_OPEN_DIRECTORY, async (event) => {
|
||
try {
|
||
const window = BrowserWindow.fromWebContents(event.sender)
|
||
if (!window) return { success: false, message: '无法找到窗口' }
|
||
|
||
const result = await dialog.showOpenDialog(window, {
|
||
properties: ['openDirectory', 'createDirectory']
|
||
})
|
||
|
||
if (result.canceled) {
|
||
return { success: false, canceled: true }
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
path: result.filePaths[0],
|
||
canceled: false
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('打开目录失败:', errorMessage)
|
||
return { success: false, message: errorMessage }
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 执行 Git Clone (使用 spawn 以支持进度流)
|
||
*/
|
||
ipcMain.handle(IPC_CHANNELS.GIT_CLONE, async (event, repoUrl: string, targetDir: string) => {
|
||
return new Promise((resolve) => {
|
||
const window = BrowserWindow.fromWebContents(event.sender)
|
||
|
||
// 检查目录是否存在
|
||
try {
|
||
if (!fsSync.existsSync(targetDir)) { // 使用同步检查避免 async/await 复杂性
|
||
fsSync.mkdirSync(targetDir, { recursive: true })
|
||
} else {
|
||
// 如果目录存在,必须为空
|
||
const files = fsSync.readdirSync(targetDir)
|
||
if (files.length > 0) {
|
||
return resolve({
|
||
success: false,
|
||
message: `目标文件夹不为空。请选择一个空文件夹。`
|
||
})
|
||
}
|
||
}
|
||
} catch (err) {
|
||
return resolve({ success: false, message: `检查目录失败: ${err}` })
|
||
}
|
||
|
||
console.log(`开始 Git Clone (spawn): ${repoUrl} -> ${targetDir}`)
|
||
|
||
// 这里的 spawn 命令不需要 util.promisify
|
||
const { spawn } = require('child_process')
|
||
const child = spawn('git', ['clone', '--progress', repoUrl, '.'], { cwd: targetDir })
|
||
|
||
let stdoutData = ''
|
||
let stderrData = ''
|
||
|
||
child.stdout.on('data', (data: any) => {
|
||
const text = data.toString()
|
||
stdoutData += text
|
||
// git 的标准输出通常是静默的,除非有错误或特定配置
|
||
console.log('Git Stdout:', text)
|
||
})
|
||
|
||
child.stderr.on('data', (data: any) => {
|
||
const text = data.toString()
|
||
stderrData += text
|
||
|
||
// 发送进度事件到前端
|
||
if (window) {
|
||
window.webContents.send('git:progress', {
|
||
repoUrl,
|
||
raw: text
|
||
})
|
||
}
|
||
})
|
||
|
||
child.on('close', (code: number) => {
|
||
console.log(`Git clone process exited with code ${code}`)
|
||
if (code === 0) {
|
||
resolve({
|
||
success: true,
|
||
message: '项目克隆成功',
|
||
stdout: stdoutData
|
||
})
|
||
} else {
|
||
resolve({
|
||
success: false,
|
||
message: `Git clone 失败 (Code ${code})`,
|
||
stdout: stderrData // git 错误通常在 stderr
|
||
})
|
||
}
|
||
})
|
||
|
||
child.on('error', (err: any) => {
|
||
console.error('Git spawn error:', err)
|
||
resolve({
|
||
success: false,
|
||
message: `启动 Git 进程失败: ${err.message}`
|
||
})
|
||
})
|
||
})
|
||
})
|
||
}
|