新增小黑盒内容爬取,AI消息自动发送评论
This commit is contained in:
+318
-72
@@ -1,19 +1,37 @@
|
||||
import { app, BrowserWindow, ipcMain, screen, globalShortcut, clipboard } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { chromium, BrowserContext } from 'playwright'
|
||||
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 floatingWindowReady = false
|
||||
let tooltipOpenCache = false
|
||||
let chatWindow: 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')
|
||||
|
||||
function createFloatingWindow(): void {
|
||||
const { width } = screen.getPrimaryDisplay().workAreaSize
|
||||
|
||||
floatingWindow = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 400,
|
||||
x: width - 420,
|
||||
y: 60,
|
||||
width: 240,
|
||||
height: 210,
|
||||
x: width - 100,
|
||||
y: 20,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
alwaysOnTop: true,
|
||||
@@ -32,63 +50,12 @@ function createFloatingWindow(): void {
|
||||
floatingWindow.setAlwaysOnTop(true, 'floating')
|
||||
floatingWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
|
||||
|
||||
// Mark window as ready when content is loaded
|
||||
floatingWindow.webContents.on('did-finish-load', () => {
|
||||
floatingWindowReady = true
|
||||
})
|
||||
|
||||
// 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 quit app from renderer
|
||||
ipcMain.on('quit-app', () => {
|
||||
app.quit()
|
||||
})
|
||||
|
||||
// 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'))
|
||||
}
|
||||
|
||||
// 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 }
|
||||
})
|
||||
|
||||
// Listen for tooltip state changes from renderer to update cache
|
||||
ipcMain.on('tooltip-state-changed', (_, isOpen: boolean) => {
|
||||
tooltipOpenCache = isOpen
|
||||
})
|
||||
|
||||
// Handle check if tooltip is open
|
||||
ipcMain.handle('is-tooltip-open', () => {
|
||||
if (floatingWindow) {
|
||||
return floatingWindow.webContents.executeJavaScript('window.__tooltipOpen || false')
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function createSettingsWindow(): void {
|
||||
@@ -123,28 +90,234 @@ function createSettingsWindow(): void {
|
||||
})
|
||||
}
|
||||
|
||||
function createChatWindow(initialText?: string): void {
|
||||
// If chat window already exists, focus it and send new text if provided
|
||||
if (chatWindow && !chatWindow.isDestroyed()) {
|
||||
chatWindow.focus()
|
||||
if (initialText) {
|
||||
chatWindow.webContents.send('set-initial-text', initialText)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
chatWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
title: 'AI 对话',
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
})
|
||||
|
||||
// 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'))
|
||||
}
|
||||
|
||||
// Send initial text after page loads
|
||||
if (initialText) {
|
||||
chatWindow.webContents.once('did-finish-load', () => {
|
||||
chatWindow?.webContents.send('set-initial-text', initialText)
|
||||
})
|
||||
}
|
||||
|
||||
chatWindow.on('closed', () => {
|
||||
chatWindow = 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
|
||||
}
|
||||
}> {
|
||||
let browser
|
||||
try {
|
||||
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()
|
||||
|
||||
// Navigate to the URL
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
|
||||
// Get appropriate scraper for this URL
|
||||
const scraper = scraperFactory.getScraper(url)
|
||||
if (!scraper) {
|
||||
throw new Error('No suitable scraper found for this URL')
|
||||
}
|
||||
|
||||
// Use scraper to extract content
|
||||
const result = await scraper.scrape(page)
|
||||
|
||||
await browser.close()
|
||||
return result
|
||||
} catch (error) {
|
||||
if (browser) {
|
||||
await browser.close()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get or create persistent browser context
|
||||
async function getPersistentContext(headless = true): Promise<BrowserContext> {
|
||||
if (!persistentContext) {
|
||||
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'
|
||||
})
|
||||
}
|
||||
return persistentContext
|
||||
}
|
||||
|
||||
// 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()
|
||||
return await service.waitForQrCodeLogin(persistentContext)
|
||||
} 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
|
||||
async function checkPlatformLogin(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 page = await context.newPage()
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
|
||||
const loginStatus = await service.checkLoginStatus(page)
|
||||
await page.close()
|
||||
|
||||
return { success: true, ...loginStatus }
|
||||
} catch (error) {
|
||||
console.error('Check platform login error:', error)
|
||||
return {
|
||||
success: false,
|
||||
isLoggedIn: false,
|
||||
error: error instanceof Error ? error.message : '检查登录状态失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 : '发送评论失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerGlobalShortcuts(): void {
|
||||
// Register Cmd+K (Mac) or Ctrl+K (Windows/Linux)
|
||||
// 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() && floatingWindowReady) {
|
||||
// Get clipboard text immediately for faster response
|
||||
if (floatingWindow && !floatingWindow.isDestroyed()) {
|
||||
// Get clipboard text for selected text
|
||||
const selectedText = clipboard.readText('selection')
|
||||
const text = selectedText || clipboard.readText()
|
||||
|
||||
// Use cached state for instant response
|
||||
if (tooltipOpenCache) {
|
||||
// If tooltip is open, close it
|
||||
tooltipOpenCache = false
|
||||
floatingWindow.webContents.send('close-tooltip')
|
||||
} else {
|
||||
// If tooltip is closed, open it with selected text
|
||||
if (text && text.trim()) {
|
||||
tooltipOpenCache = true
|
||||
floatingWindow.webContents.send('show-text-action-prompt', text)
|
||||
floatingWindow.focus()
|
||||
}
|
||||
if (text && text.trim()) {
|
||||
// Send event to renderer to show prompt
|
||||
floatingWindow.webContents.send('show-text-prompt', text.trim())
|
||||
floatingWindow.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -161,6 +334,79 @@ 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 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
|
||||
ipcMain.handle('check-platform-login', async (_, url: string) => {
|
||||
return await checkPlatformLogin(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()
|
||||
})
|
||||
|
||||
createFloatingWindow()
|
||||
registerGlobalShortcuts()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user