mirror3/electron/main.ts
2025-03-13 23:49:11 +08:00

320 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// electron/main.ts
import { app, BrowserWindow, Menu, globalShortcut, net, ipcMain } from 'electron';
import path from 'path';
import http from 'http';
import { URL } from 'url';
//import { apiRoutes } from './api'; // 导入API路由配置
import dotenv from 'dotenv';
// 添加openai模块
import OpenAI from 'openai';
dotenv.config({ path: path.join(__dirname, '../.env') }); // 根据实际路径调整
// 确保在news.ts中能读取到
console.log('JUHE_KEY:', process.env.JUHE_NEWS_KEY?.substring(0, 3) + '***'); // 打印前3位验证
console.log('CAIYUN_KEY:', process.env.CAIYUN_API_KEY?.substring(0, 3) + '***'); // 打印前3位验证
// 环境配置
const isDev = process.env.NODE_ENV === 'development';
// 注册 IPC 处理器
function registerIpcHandlers() {
// 天气接口(需要修改)
ipcMain.handle('get-weather', async (_, { lon, lat }) => {
console.log('[IPC] 开始获取天气数据', { lon, lat });
// 检查缓存有效性
const now = Date.now();
if (now - apiCache.weather.timestamp < apiCache.weather.ttl) {
console.log('[Cache] 返回缓存的天气数据');
return apiCache.weather.data;
}
try {
const apiKey = process.env.CAIYUN_API_KEY!;
const apiUrl = `https://api.caiyunapp.com/v2.6/${apiKey}/${lon},${lat}/weather`;
const response = await net.fetch(apiUrl);
const jsonData = await response.json();
if (jsonData.status !== 'ok') {
throw new Error(jsonData.message || '天气数据获取失败');
}
// 详细数据格式化
const formattedData = {
realtime: formatRealtime(jsonData.result.realtime),
forecast: formatDailyForecast(jsonData.result.daily)
};
console.log('[IPC] 格式化天气数据:', formattedData);
// 更新缓存
apiCache.weather.data = formattedData;
apiCache.weather.timestamp = now;
return formattedData;
} catch (error: any) {
console.error('[IPC] 天气请求失败:', error);
return {
error: true,
message: error.message
};
}
});
// 实时天气格式化
const formatRealtime = (data: any) => ({
temperature: data.temperature.toFixed(1),
humidity: (data.humidity * 100).toFixed(1) + '%',
wind: {
speed: (data.wind.speed * 3.6).toFixed(1) + 'km/h',
direction: getWindDirection(data.wind.direction)
},
airQuality: {
aqi: data.air_quality.aqi.chn,
description: data.air_quality.description.chn
},
skycon: data.skycon,
apparentTemperature: data.apparent_temperature.toFixed(1)
});
// 天气预报格式化
const formatDailyForecast = (data: any) => ({
temperature: data.temperature.map((item: any) => ({
date: item.date,
max: item.max,
min: item.min
})),
skycon: data.skycon,
precipitation: data.precipitation
});
// 风向转换
const getWindDirection = (degrees: number) => {
const directions = ['北风', '东北风', '东风', '东南风', '南风', '西南风', '西风', '西北风'];
return directions[Math.round(degrees % 360 / 45) % 8];
};
// 新闻接口
ipcMain.handle('get-news', async () => {
console.log('[IPC] 开始获取新闻数据');
const now = Date.now();
if (now - apiCache.news.timestamp < apiCache.news.ttl) {
console.log('[Cache] 返回缓存的新闻数据');
return apiCache.news.data;
}
try {
const apiKey = process.env.JUHE_NEWS_KEY!;
const response = await net.fetch(
`https://v.juhe.cn/toutiao/index?type=top&key=${apiKey}`
);
const json = await response.json();
console.log('[IPC] 新闻响应:', json); // 添加原始响应日志
const formatted = formatNews(json.result?.data || []);
// 更新缓存
apiCache.news.data = formatted;
apiCache.news.timestamp = now;
return formatted;
//return formatNews(json.result?.data || []);
} catch (error) {
console.error('[IPC] 新闻请求失败:', error);
throw error;
}
});
// 聊天接口
ipcMain.handle('chat-with-deepseek', async (event, { messages }) => {
try {
const openai = new OpenAI({
baseURL: 'https://api.deepseek.com',
apiKey: process.env.DEEPSEEK_API_KEY!,
});
const stream = await openai.chat.completions.create({
model: 'deepseek-chat',
messages,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
event.sender.send('chat-stream-chunk', content);
}
event.sender.send('chat-stream-end');
} catch (error: any) {
console.error('Deepseek API Error:', error);
event.sender.send('chat-stream-error', error.message);
throw error;
}
});
}
const formatNews = (items: any[]) => items.map(item => ({
uniquekey: item.uniquekey,
title: item.title,
date: item.date,
category: item.category || "头条新闻",
author_name: item.author_name || "未知作者",
url: item.url,
thumbnail_pic_s: item.thumbnail_pic_s,
is_content: item.is_content
}));
// 添加缓存对象
const apiCache = {
weather: {
data: null as any,
timestamp: 0,
//ttl: 1800000 // 30分钟缓存
//6小时缓存
ttl: 21600000
},
news: {
data: null as any,
timestamp: 0,
//ttl: 3600000 // 1小时缓存
//2小时缓存
ttl: 7200000
}
};
// 禁用默认菜单(提升安全性和专业性)
Menu.setApplicationMenu(null);
/**
* 创建主窗口
*/
async function createWindow() {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 启用上下文隔离(安全必需)
nodeIntegration: false, // 禁用Node集成安全要求
webSecurity: !isDev, // 生产环境启用安全策略
sandbox: true // 启用沙箱模式
},
show: false // 先隐藏窗口直到内容加载完成
});
// 优化加载体验
mainWindow.once('ready-to-show', () => {
mainWindow.show();
if (isDev) {
mainWindow.webContents.openDevTools({ mode: 'detach' }); // 打开开发者工具
console.log('Developer tools opened in detached mode');
}
});
// 新增权限处理代码必须在loadURL之前
const ses = mainWindow.webContents.session;
// 自动处理权限请求
ses.setPermissionRequestHandler((webContents, permission, callback) => {
const allowedPermissions = new Set(['media', 'microphone', 'audioCapture']);
callback(allowedPermissions.has(permission));
});
// 自动授予设备权限
// 替换原来的setDevicePermissionHandler
ses.setPermissionRequestHandler((webContents, permission, callback) => {
// 统一允许所有媒体相关权限
if (['microphone', 'audioCapture', 'media'].includes(permission)) {
return callback(true);
}
callback(false);
});
// 加载内容
const loadURL = isDev
? 'http://localhost:5173'
: `file://${path.join(__dirname, '../dist/index.html')}`;
console.log(`Loading URL: ${loadURL}`);
await mainWindow.loadURL(loadURL);
registerGlobalShortcuts(mainWindow);
return mainWindow;
}
/**
* 统一响应格式(带安全头)
*/
function sendResponse(
res: http.ServerResponse,
status: number,
data: unknown,
headers: Record<string, string> = {}
) {
const securityHeaders = {
'Content-Security-Policy': "default-src 'self'",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY'
};
const responseHeaders = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
...securityHeaders,
...headers
};
res.writeHead(status, responseHeaders);
res.end(JSON.stringify(data));
}
/**
* 注册全局快捷键
*/
function registerGlobalShortcuts(win: BrowserWindow) {
const shortcut = process.platform === 'darwin' ? 'Command+Q' : 'Ctrl+Q';
const ret = globalShortcut.register(shortcut, () => {
console.log('Gracefully shutting down...');
win.destroy();
app.quit();
});
if (!ret) {
console.error('Failed to register shortcut');
}
}
/**
* 应用启动流程
*/
app.whenReady()
.then(() => {
// 注册 IPC 处理器的代码
registerIpcHandlers();
console.log('Creating browser window...');
return createWindow();
})
.catch((error) => {
console.error('App initialization failed:', error);
app.quit();
});
/**
* 生命周期管理
*/
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('will-quit', () => {
globalShortcut.unregisterAll();
});