Files
ai-desktop/src/main/index.ts
T
2025-11-24 14:47:03 +08:00

973 lines
29 KiB
TypeScript

import { app, BrowserWindow, ipcMain, screen, globalShortcut, clipboard } from 'electron'
import { join } from 'path'
import { chromium, BrowserContext } from 'playwright'
import { existsSync, rmSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
import { ScraperFactory } from './scrapers'
import { XiaoheiheScrap } from './scrapers/xiaoheihe'
import { GenericScraper } from './scrapers/generic'
import { PlatformServiceFactory } from './platforms'
import { XiaoheiheService } from './platforms/xiaoheihe'
let floatingWindow: BrowserWindow | null = null
let settingsWindow: BrowserWindow | null = null
let chatWindow: BrowserWindow | null = null
let toolsPanelWindow: BrowserWindow | null = null
// Initialize scraper factory
const scraperFactory = new ScraperFactory()
scraperFactory.register(new XiaoheiheScrap())
scraperFactory.register(new GenericScraper()) // Generic must be last (catch-all)
// Initialize platform service factory
const platformServiceFactory = new PlatformServiceFactory()
platformServiceFactory.register(new XiaoheiheService())
// Persistent browser context for maintaining login state
let persistentContext: BrowserContext | null = null
const userDataDir = join(app.getPath('userData'), 'browser-data')
let contextInitializing = false
let contextInitRetries = 0
const MAX_CONTEXT_INIT_RETRIES = 3
// Settings file path
const settingsDir = join(app.getPath('userData'), 'settings')
const settingsFilePath = join(settingsDir, 'config.json')
const loginInfoFilePath = join(settingsDir, 'login-info.json')
// Constants
const RATE_LIMIT_CONFIG = {
MIN_INTERVAL_MS: 3000, // 最小间隔 3 秒
MAX_SEARCH_PER_MINUTE: 10, // 每分钟最多 10 次
RESET_INTERVAL_MS: 60000, // 1 分钟重置计数
PAGE_TIMEOUT_MS: 30000, // 页面加载超时 30 秒
BROWSER_INIT_TIMEOUT_MS: 30000 // 浏览器初始化超时 30 秒
} as const
// Rate limiter for search operations
class SearchRateLimiter {
private lastSearchTime: number = 0
private searchCount: number = 0
private readonly minInterval: number = RATE_LIMIT_CONFIG.MIN_INTERVAL_MS
private readonly maxSearchPerMinute: number = RATE_LIMIT_CONFIG.MAX_SEARCH_PER_MINUTE
private readonly resetInterval: number = RATE_LIMIT_CONFIG.RESET_INTERVAL_MS
canSearch(): { allowed: boolean; waitTime?: number; reason?: string } {
const now = Date.now()
const timeSinceLastSearch = now - this.lastSearchTime
// 检查是否需要重置计数器
if (timeSinceLastSearch > this.resetInterval) {
this.searchCount = 0
}
// 检查是否超过频率限制
if (this.searchCount >= this.maxSearchPerMinute) {
const waitTime = this.resetInterval - timeSinceLastSearch
return {
allowed: false,
waitTime: Math.ceil(waitTime / 1000),
reason: '搜索过于频繁,请稍后再试'
}
}
// 检查最小间隔
if (timeSinceLastSearch < this.minInterval) {
const waitTime = this.minInterval - timeSinceLastSearch
return {
allowed: false,
waitTime: Math.ceil(waitTime / 1000),
reason: '请求过快,请稍后再试'
}
}
return { allowed: true }
}
recordSearch(): void {
this.lastSearchTime = Date.now()
this.searchCount++
}
reset(): void {
this.searchCount = 0
this.lastSearchTime = 0
}
}
const searchRateLimiter = new SearchRateLimiter()
// Ensure settings directory exists
function ensureSettingsDir(): void {
if (!existsSync(settingsDir)) {
mkdirSync(settingsDir, { recursive: true })
}
}
// Read settings from file
function readSettings(): any {
try {
ensureSettingsDir()
if (existsSync(settingsFilePath)) {
const data = readFileSync(settingsFilePath, 'utf-8')
return JSON.parse(data)
}
return {}
} catch (error) {
console.error('Failed to read settings:', error)
return {}
}
}
// Write settings to file
function writeSettings(settings: any): { success: boolean; error?: string } {
try {
ensureSettingsDir()
writeFileSync(settingsFilePath, JSON.stringify(settings, null, 2), 'utf-8')
return { success: true }
} catch (error) {
console.error('Failed to write settings:', error)
return {
success: false,
error: error instanceof Error ? error.message : '保存设置失败'
}
}
}
// Read login info from file
function readLoginInfo(): any {
try {
ensureSettingsDir()
if (existsSync(loginInfoFilePath)) {
const data = readFileSync(loginInfoFilePath, 'utf-8')
return JSON.parse(data)
}
return {}
} catch (error) {
console.error('Failed to read login info:', error)
return {}
}
}
// Write login info to file
function writeLoginInfo(loginInfo: any): { success: boolean; error?: string } {
try {
ensureSettingsDir()
writeFileSync(loginInfoFilePath, JSON.stringify(loginInfo, null, 2), 'utf-8')
return { success: true }
} catch (error) {
console.error('Failed to write login info:', error)
return {
success: false,
error: error instanceof Error ? error.message : '保存登录信息失败'
}
}
}
function createFloatingWindow(): void {
const { width, height } = screen.getPrimaryDisplay().workAreaSize
floatingWindow = new BrowserWindow({
width: 160,
height: 200,
x: width - 160,
y: Math.floor((height - 200) / 2),
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
hasShadow: false,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: false,
contextIsolation: true
}
})
floatingWindow.setIgnoreMouseEvents(true, { forward: true })
floatingWindow.setAlwaysOnTop(true, 'floating')
floatingWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
// Load the floating window HTML
if (process.env['ELECTRON_RENDERER_URL']) {
floatingWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/floating.html`)
} else {
floatingWindow.loadFile(join(__dirname, '../renderer/floating.html'))
}
}
function createSettingsWindow(): void {
// If settings window already exists, focus it
if (settingsWindow && !settingsWindow.isDestroyed()) {
settingsWindow.focus()
return
}
settingsWindow = new BrowserWindow({
width: 900,
height: 600,
title: '设置',
resizable: false,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: false,
contextIsolation: true
}
})
// Load settings page
if (process.env['ELECTRON_RENDERER_URL']) {
settingsWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/settings.html`)
} else {
settingsWindow.loadFile(join(__dirname, '../renderer/settings.html'))
}
settingsWindow.on('closed', () => {
settingsWindow = null
})
}
function createChatWindow(initialText?: string): void {
const startTime = Date.now()
console.log('[PERF] createChatWindow called at:', startTime)
// If chat window already exists, focus it and send new text if provided
if (chatWindow && !chatWindow.isDestroyed()) {
console.log('Chat window already exists, focusing and sending text')
chatWindow.focus()
if (initialText) {
// Add a small delay to ensure the renderer is ready
setTimeout(() => {
chatWindow?.webContents.send('set-initial-text', initialText)
console.log('Sent initial text to existing window:', initialText)
}, 100)
}
return
}
console.log('[PERF] Creating new chat window')
chatWindow = new BrowserWindow({
width: 800,
height: 600,
title: 'AI 对话',
show: false, // Hide window during load for better perceived performance
backgroundColor: '#ffffff', // Set background color to avoid flash
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: false,
contextIsolation: true,
spellcheck: false, // Disable spell check to avoid conflicts with IME
backgroundThrottling: false // Prevent throttling for better performance
}
})
// Load chat page
if (process.env['ELECTRON_RENDERER_URL']) {
chatWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/chat.html`)
} else {
chatWindow.loadFile(join(__dirname, '../renderer/chat.html'))
}
// Show window when ready to avoid showing loading state
chatWindow.once('ready-to-show', () => {
const readyTime = Date.now()
console.log('[PERF] Chat window ready-to-show at:', readyTime)
console.log('[PERF] Time from create to ready:', readyTime - startTime, 'ms')
chatWindow?.show()
})
// Send initial text after page loads
if (initialText) {
console.log('Setting up did-finish-load listener for initial text')
chatWindow.webContents.once('did-finish-load', () => {
console.log('Chat window did-finish-load event fired')
// Add a small delay to ensure Vue components are mounted
setTimeout(() => {
if (chatWindow && !chatWindow.isDestroyed()) {
console.log('Sending initial text to new window:', initialText)
chatWindow.webContents.send('set-initial-text', initialText)
}
}, 200)
})
}
chatWindow.on('closed', () => {
chatWindow = null
})
}
// Create tools panel window
function createToolsPanelWindow(): void {
console.log('Creating tools panel window')
// If tools panel already exists, focus it
if (toolsPanelWindow && !toolsPanelWindow.isDestroyed()) {
console.log('Tools panel window already exists, focusing')
toolsPanelWindow.focus()
return
}
toolsPanelWindow = new BrowserWindow({
width: 900,
height: 700,
title: '工具箱',
show: false,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: false,
contextIsolation: true,
spellcheck: false
}
})
// Load tools panel page
if (process.env['ELECTRON_RENDERER_URL']) {
toolsPanelWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/tools.html`)
} else {
toolsPanelWindow.loadFile(join(__dirname, '../renderer/tools.html'))
}
// Show window when ready
toolsPanelWindow.once('ready-to-show', () => {
console.log('Tools panel window ready-to-show')
toolsPanelWindow?.show()
})
toolsPanelWindow.on('closed', () => {
toolsPanelWindow = null
})
}
// Fetch article content using Playwright with factory pattern
async function fetchArticleContent(url: string): Promise<{
title: string
author?: string
authorIp?: string
publishTime?: string
content: string
tags?: string[]
comments: Array<{
author: string
content: string
time?: string
replies?: Array<{ author: string; content: string; time?: string }>
}>
stats?: {
likes: number
favorites: number
commentCount: number
hotScore: number
}
}> {
console.log('fetchArticleContent: Starting to fetch article from URL:', url)
let browser
try {
console.log('fetchArticleContent: Launching browser...')
browser = await chromium.launch({ headless: true })
const context = await browser.newContext({
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
})
const page = await context.newPage()
console.log('fetchArticleContent: Navigating to URL...')
// Navigate to the URL
await page.goto(url, { waitUntil: 'networkidle', timeout: RATE_LIMIT_CONFIG.PAGE_TIMEOUT_MS })
// Get appropriate scraper for this URL
console.log('fetchArticleContent: Getting scraper for URL...')
const scraper = scraperFactory.getScraper(url)
if (!scraper) {
throw new Error('No suitable scraper found for this URL')
}
console.log('fetchArticleContent: Using scraper:', scraper.constructor.name)
// Use scraper to extract content
const result = await scraper.scrape(page)
console.log('fetchArticleContent: Scraping completed, article title:', result.title)
return result
} catch (error) {
console.error('fetchArticleContent: Error occurred:', error)
throw error
} finally {
// Always close browser in finally block to ensure cleanup
if (browser) {
try {
await browser.close()
console.log('fetchArticleContent: Browser closed')
} catch (closeError) {
console.error('Failed to close browser:', closeError)
}
}
}
}
// Get or create persistent browser context
async function getPersistentContext(headless = true): Promise<BrowserContext> {
// Return existing context if available and not closed
if (persistentContext && !persistentContext.pages().length) {
console.log('Browser context exists but has no pages, reinitializing')
try {
await persistentContext.close()
} catch (error) {
console.error('Error closing stale context:', error)
}
persistentContext = null
}
if (persistentContext) {
try {
// Test if context is still alive
await persistentContext.pages()
return persistentContext
} catch (error) {
console.error('Browser context is dead, reinitializing:', error)
persistentContext = null
}
}
// Prevent multiple simultaneous initialization attempts
if (contextInitializing) {
console.log('Context initialization in progress, waiting...')
// Wait for initialization to complete
while (contextInitializing && contextInitRetries < MAX_CONTEXT_INIT_RETRIES) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
if (persistentContext) {
return persistentContext
}
}
// Initialize new context with retry logic
contextInitializing = true
let lastError: Error | null = null
for (let attempt = 1; attempt <= MAX_CONTEXT_INIT_RETRIES; attempt++) {
try {
console.log(`Initializing browser context (attempt ${attempt}/${MAX_CONTEXT_INIT_RETRIES})`)
persistentContext = await chromium.launchPersistentContext(userDataDir, {
headless,
viewport: { width: 1280, height: 800 },
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
// Add timeout for initialization
timeout: RATE_LIMIT_CONFIG.BROWSER_INIT_TIMEOUT_MS
})
// Setup crash handler
persistentContext.on('close', () => {
console.log('Browser context closed')
persistentContext = null
contextInitRetries = 0
})
console.log('Browser context initialized successfully')
contextInitializing = false
contextInitRetries = 0
return persistentContext
} catch (error) {
lastError = error as Error
console.error(`Browser context initialization failed (attempt ${attempt}):`, error)
// Clean up failed context
if (persistentContext) {
try {
await persistentContext.close()
} catch (closeError) {
console.error('Error closing failed context:', closeError)
}
persistentContext = null
}
// Wait before retry (exponential backoff)
if (attempt < MAX_CONTEXT_INIT_RETRIES) {
const delayMs = 1000 * attempt
console.log(`Waiting ${delayMs}ms before retry...`)
await new Promise(resolve => setTimeout(resolve, delayMs))
}
}
}
contextInitializing = false
contextInitRetries++
throw new Error(
`Failed to initialize browser context after ${MAX_CONTEXT_INIT_RETRIES} attempts: ${lastError?.message}`
)
}
// Get login QR code
async function getLoginQrCode(): Promise<{
success: boolean
qrCodeDataUrl?: string
error?: string
}> {
try {
// 使用无头模式
const context = await getPersistentContext(true)
const service = new XiaoheiheService()
return await service.getLoginQrCode(context)
} catch (error) {
console.error('Get login QR code error:', error)
return {
success: false,
error: error instanceof Error ? error.message : '获取二维码失败'
}
}
}
// Wait for QR code login
async function waitForQrCodeLogin(): Promise<{
success: boolean
username?: string
error?: string
}> {
try {
if (!persistentContext) {
return { success: false, error: '浏览器上下文未初始化' }
}
const service = new XiaoheiheService()
const result = await service.waitForQrCodeLogin(persistentContext)
// Save login info if successful
if (result.success && result.username) {
const savedLoginInfo = readLoginInfo()
savedLoginInfo['www.xiaoheihe.cn'] = {
username: result.username,
lastUpdate: new Date().toISOString()
}
writeLoginInfo(savedLoginInfo)
console.log('Login info saved for user:', result.username)
}
return result
} catch (error) {
console.error('Wait for QR code login error:', error)
return {
success: false,
error: error instanceof Error ? error.message : '等待登录失败'
}
}
}
// Check login status for a platform (fast - cookie-based only)
async function checkPlatformLoginFast(url: string): Promise<{
success: boolean
isLoggedIn: boolean
username?: string
error?: string
}> {
try {
const service = platformServiceFactory.getService(url)
if (!service) {
return { success: false, isLoggedIn: false, error: '不支持的平台' }
}
const context = await getPersistentContext()
const loginStatus = await service.checkLoginStatusFast(context)
// 如果已登录,尝试从本地文件读取用户名(如果 service 没有返回用户名)
if (loginStatus.isLoggedIn && !loginStatus.username) {
const savedLoginInfo = readLoginInfo()
const platformKey = url.replace(/https?:\/\//, '').split('/')[0]
if (savedLoginInfo[platformKey]?.username) {
loginStatus.username = savedLoginInfo[platformKey].username
}
}
// 如果已登录且有用户名,保存到本地文件
if (loginStatus.isLoggedIn && loginStatus.username) {
const savedLoginInfo = readLoginInfo()
const platformKey = url.replace(/https?:\/\//, '').split('/')[0]
savedLoginInfo[platformKey] = {
username: loginStatus.username,
lastUpdate: new Date().toISOString()
}
writeLoginInfo(savedLoginInfo)
}
return { success: true, ...loginStatus }
} catch (error) {
console.error('Check platform login fast error:', error)
return {
success: false,
isLoggedIn: false,
error: error instanceof Error ? error.message : '检查登录状态失败'
}
}
}
// Check login status for a platform
async function checkPlatformLogin(url: string): Promise<{
success: boolean
isLoggedIn: boolean
username?: string
error?: string
}> {
let page: Awaited<ReturnType<BrowserContext['newPage']>> | undefined
try {
const service = platformServiceFactory.getService(url)
if (!service) {
return { success: false, isLoggedIn: false, error: '不支持的平台' }
}
const context = await getPersistentContext()
page = await context.newPage()
await page.goto(url, { waitUntil: 'networkidle', timeout: RATE_LIMIT_CONFIG.PAGE_TIMEOUT_MS })
const loginStatus = await service.checkLoginStatus(page)
return { success: true, ...loginStatus }
} catch (error) {
console.error('Check platform login error:', error)
return {
success: false,
isLoggedIn: false,
error: error instanceof Error ? error.message : '检查登录状态失败'
}
} finally {
// Always close page in finally block to ensure cleanup
if (page) {
try {
await page.close()
} catch (closeError) {
console.error('Failed to close page:', closeError)
}
}
}
}
// Post comment to platform
async function postCommentToPlatform(
url: string,
comment: string
): Promise<{ success: boolean; error?: string }> {
try {
console.log('postCommentToPlatform: Starting with URL:', url)
console.log('postCommentToPlatform: Comment length:', comment?.length)
const service = platformServiceFactory.getService(url)
if (!service) {
console.error('postCommentToPlatform: No service found for URL')
return { success: false, error: '不支持的平台' }
}
console.log('postCommentToPlatform: Service found')
console.log('postCommentToPlatform: Getting persistent context...')
const context = await getPersistentContext()
console.log('postCommentToPlatform: Context obtained, calling service.postComment...')
const result = await service.postComment(context, url, comment)
console.log('postCommentToPlatform: Result from service.postComment:', JSON.stringify(result))
return result
} catch (error) {
console.error('postCommentToPlatform: Exception occurred:', error)
console.error(
'postCommentToPlatform: Error stack:',
error instanceof Error ? error.stack : 'No stack trace'
)
return {
success: false,
error: error instanceof Error ? error.message : '发送评论失败'
}
}
}
// Search platform content
async function searchPlatform(
platform: string,
query: string
): Promise<{
success: boolean
results?: Array<{
title: string
url: string
author?: string
publishTime?: string
summary?: string
commentCount?: number
likeCount?: number
}>
error?: string
}> {
try {
console.log('searchPlatform: Starting search on platform:', platform)
console.log('searchPlatform: Query:', query)
// 检查搜索频率限制
const rateLimitCheck = searchRateLimiter.canSearch()
if (!rateLimitCheck.allowed) {
console.log('searchPlatform: Rate limit exceeded')
return {
success: false,
error: 'RATE_LIMIT_EXCEEDED',
results: []
}
}
// 根据平台名称构造一个URL来获取对应的服务
const platformUrls: Record<string, string> = {
xiaoheihe: 'https://www.xiaoheihe.cn'
}
const platformUrl = platformUrls[platform]
if (!platformUrl) {
return { success: false, error: '不支持的平台' }
}
const service = platformServiceFactory.getService(platformUrl)
if (!service) {
return { success: false, error: '未找到平台服务' }
}
console.log('searchPlatform: Getting persistent context...')
const context = await getPersistentContext()
console.log('searchPlatform: Context obtained, calling service.search...')
// 记录本次搜索
searchRateLimiter.recordSearch()
const result = await service.search(context, query)
console.log('searchPlatform: Search completed, found', result.results?.length || 0, 'results')
return result
} catch (error) {
console.error('searchPlatform: Exception occurred:', error)
return {
success: false,
error: error instanceof Error ? error.message : '搜索失败'
}
}
}
function registerGlobalShortcuts(): void {
// Register Command+K (Mac) or Ctrl+K (Windows/Linux)
const shortcut = process.platform === 'darwin' ? 'Command+K' : 'Control+K'
const registered = globalShortcut.register(shortcut, () => {
if (floatingWindow && !floatingWindow.isDestroyed()) {
// Read clipboard content (user should copy text with Command+C first)
// We only read the main clipboard, not the selection clipboard
const text = clipboard.readText()
console.log('Command+K pressed, clipboard content:', text?.substring(0, 50))
// Always send the event to toggle action menu
// Pass clipboard text (empty string if clipboard is empty)
floatingWindow.webContents.send('show-text-prompt', text || '')
floatingWindow.focus()
}
})
if (!registered) {
console.error('Global shortcut registration failed')
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// IPC test
ipcMain.on('ping', () => console.log('pong'))
// Handle mouse enter/leave events to control click-through
ipcMain.on('set-ignore-mouse-events', (_, ignore: boolean, options?: { forward: boolean }) => {
if (floatingWindow) {
floatingWindow.setIgnoreMouseEvents(ignore, options)
}
})
// Handle open settings from renderer
ipcMain.on('open-settings', () => {
createSettingsWindow()
})
// Handle open tools panel from renderer
ipcMain.on('open-tools-panel', () => {
createToolsPanelWindow()
})
// Handle close tools panel from renderer
ipcMain.on('close-tools-panel', () => {
if (toolsPanelWindow && !toolsPanelWindow.isDestroyed()) {
toolsPanelWindow.close()
}
})
// Handle open chat window from renderer
ipcMain.on('open-chat', (_, selectedText?: string) => {
console.log('open-chat event received, selectedText:', selectedText)
createChatWindow(selectedText)
})
// Handle quit app from renderer
ipcMain.on('quit-app', () => {
app.quit()
})
// Handle window drag
ipcMain.on('floating-window-move', (_, { x, y }) => {
if (floatingWindow) {
floatingWindow.setPosition(x, y)
}
})
// Handle get window bounds
ipcMain.handle('get-window-bounds', () => {
if (floatingWindow) {
const bounds = floatingWindow.getBounds()
return { x: bounds.x, y: bounds.y }
}
return { x: 0, y: 0 }
})
// Handle fetch article content
ipcMain.handle('fetch-article', async (_, url: string) => {
try {
const result = await fetchArticleContent(url)
return { success: true, ...result }
} catch (error) {
console.error('Failed to fetch article:', error)
return {
success: false,
error: error instanceof Error ? error.message : '抓取文章失败'
}
}
})
// Handle check platform login status (fast - cookie-based only)
ipcMain.handle('check-platform-login-fast', async (_, url: string) => {
return await checkPlatformLoginFast(url)
})
// Handle check platform login status (accepts platform name or URL)
ipcMain.handle('check-platform-login', async (_, arg: string | { platform: string }) => {
// Support both old format (URL string) and new format (object with platform)
let url: string
if (typeof arg === 'string') {
url = arg
} else {
const platformUrls: Record<string, string> = {
xiaoheihe: 'https://www.xiaoheihe.cn'
}
url = platformUrls[arg.platform]
if (!url) {
return { success: false, isLoggedIn: false, error: '不支持的平台' }
}
}
return await checkPlatformLoginFast(url)
})
// Handle post comment to platform
ipcMain.handle('post-comment', async (_, { url, comment }: { url: string; comment: string }) => {
return await postCommentToPlatform(url, comment)
})
// Handle get login QR code
ipcMain.handle('get-login-qrcode', async () => {
return await getLoginQrCode()
})
// Handle wait for QR code login
ipcMain.handle('wait-qrcode-login', async () => {
return await waitForQrCodeLogin()
})
// Handle logout
ipcMain.handle('logout-platform', async (_, platform: string) => {
try {
if (platform === 'xiaoheihe') {
// Close the persistent context to clear cookies and session
if (persistentContext) {
await persistentContext.close()
persistentContext = null
}
// Delete the user data directory to completely clear all browser data
if (existsSync(userDataDir)) {
console.log('Deleting user data directory:', userDataDir)
rmSync(userDataDir, { recursive: true, force: true })
console.log('User data directory deleted successfully')
}
// Clear saved login info from file
const savedLoginInfo = readLoginInfo()
delete savedLoginInfo['www.xiaoheihe.cn']
writeLoginInfo(savedLoginInfo)
console.log('Login info cleared for xiaoheihe')
return { success: true }
}
return { success: false, error: '不支持的平台' }
} catch (error) {
console.error('Logout error:', error)
return {
success: false,
error: error instanceof Error ? error.message : '退出登录失败'
}
}
})
// Handle search platform
ipcMain.handle(
'search-platform',
async (_, { platform, query }: { platform: string; query: string }) => {
return await searchPlatform(platform, query)
}
)
// Handle read settings
ipcMain.handle('read-settings', () => {
return readSettings()
})
// Handle write settings
ipcMain.handle('write-settings', (_, settings: any) => {
return writeSettings(settings)
})
createFloatingWindow()
registerGlobalShortcuts()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createFloatingWindow()
}
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// Unregister all shortcuts when app is about to quit
app.on('will-quit', async () => {
globalShortcut.unregisterAll()
// Clean up browser context
if (persistentContext) {
console.log('Cleaning up browser context on app quit')
try {
await persistentContext.close()
persistentContext = null
} catch (error) {
console.error('Error closing browser context on quit:', error)
}
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.