feat(electron): 添加系统级对话框功能

- 在 ElectronAPI 中新增对话框相关方法:showQuestionDialog、dialogResponse、resizeDialogWindow
- 实现独立的对话框窗口 dialog.html,支持自定义标题、消息和选项- 添加对话框窗口的键盘导航和焦点管理功能
- 在主进程中实现对话框窗口的创建、显示和响应处理
- 更新 WebSocket 消息监听器组件,使用系统级对话框替代应用内弹窗- 优化进程清理逻辑,增加多种清理方法并行执行
- 重构日志工具类,改进 Electron API 调用方式
- 调整 Git 更新检查逻辑,避免直接访问 GitHub
- 移除冗余的类型定义文件,统一 Electron API 接口定义
This commit is contained in:
MoeSnowyFox
2025-10-01 23:17:23 +08:00
parent 3d204980a2
commit 1d91204842
8 changed files with 799 additions and 409 deletions

View File

@@ -1,18 +1,28 @@
import {
app,
BrowserWindow,
ipcMain,
dialog,
shell,
Tray,
ipcMain,
Menu,
nativeImage,
screen,
shell,
Tray,
} from 'electron'
import * as path from 'path'
import * as fs from 'fs'
import { spawn, exec } from 'child_process'
import { getAppRoot, checkEnvironment } from './services/environmentService'
import { exec, spawn } from 'child_process'
import { checkEnvironment, getAppRoot } from './services/environmentService'
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
import {
downloadPython,
installDependencies,
installPipPackage,
setMainWindow as setPythonMainWindow,
startBackend,
} from './services/pythonService'
import { cloneBackend, downloadGit, setMainWindow as setGitMainWindow } from './services/gitService'
import { cleanOldLogs, getLogFiles, getLogPath, log, setupLogger } from './services/logService'
// 强制清理相关进程的函数
async function forceKillRelatedProcesses(): Promise<void> {
@@ -22,15 +32,15 @@ async function forceKillRelatedProcesses(): Promise<void> {
log.info('所有相关进程已清理')
} catch (error) {
log.error('清理进程时出错:', error)
// 备用清理方法
if (process.platform === 'win32') {
const appRoot = getAppRoot()
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
return new Promise((resolve) => {
return new Promise(resolve => {
// 使用更简单的命令强制结束相关进程
exec(`taskkill /f /im python.exe`, (error) => {
exec(`taskkill /f /im python.exe`, error => {
if (error) {
log.warn('备用清理方法失败:', error.message)
} else {
@@ -42,16 +52,6 @@ async function forceKillRelatedProcesses(): Promise<void> {
}
}
}
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
import {
setMainWindow as setPythonMainWindow,
downloadPython,
installPipPackage,
installDependencies,
startBackend,
} from './services/pythonService'
import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService'
import { setupLogger, log, getLogPath, getLogFiles, cleanOldLogs } from './services/logService'
// 检查是否以管理员权限运行
function isRunningAsAdmin(): boolean {
@@ -113,6 +113,7 @@ interface AppConfig {
IfMinimizeDirectly: boolean
IfSelfStart: boolean
}
[key: string]: any
}
@@ -304,7 +305,9 @@ function updateTrayVisibility(config: AppConfig) {
log.info('托盘图标已销毁')
}
}
let mainWindow: Electron.BrowserWindow | null = null
function createWindow() {
log.info('开始创建主窗口')
@@ -442,7 +445,7 @@ function createWindow() {
screen.removeListener('display-metrics-changed', recomputeMinSize)
// 置空模块级引用
mainWindow = null
// 如果是正在退出,立即执行进程清理
if (isQuitting) {
log.info('窗口关闭,执行最终清理')
@@ -598,19 +601,19 @@ ipcMain.handle('kill-all-processes', async () => {
ipcMain.handle('force-exit', async () => {
log.info('收到强制退出命令')
isQuitting = true
// 立即清理进程
try {
await forceKillRelatedProcesses()
} catch (e) {
log.error('强制清理失败:', e)
}
// 强制退出
setTimeout(() => {
process.exit(0)
}, 500)
return { success: true }
})
@@ -746,6 +749,137 @@ ipcMain.handle('stop-backend', async () => {
return stopBackend()
})
// 全局存储对话框窗口引用和回调
let dialogWindows = new Map<string, BrowserWindow>()
let dialogCallbacks = new Map<string, (result: boolean) => void>()
// 创建对话框窗口
function createQuestionDialog(questionData: any): Promise<boolean> {
return new Promise((resolve) => {
const messageId = questionData.messageId || 'dialog_' + Date.now()
// 存储回调函数
dialogCallbacks.set(messageId, resolve)
// 准备对话框数据
const dialogData = {
title: questionData.title || '操作确认',
message: questionData.message || '是否要执行此操作?',
options: questionData.options || ['确定', '取消'],
messageId: messageId
}
// 创建对话框窗口
const dialogWindow = new BrowserWindow({
width: 450,
height: 200,
minWidth: 350,
minHeight: 150,
maxWidth: 600,
maxHeight: 400,
resizable: true,
minimizable: false,
maximizable: false,
alwaysOnTop: true,
show: false,
frame: false,
modal: mainWindow ? true : false,
parent: mainWindow || undefined,
icon: path.join(__dirname, '../public/AUTO-MAS.ico'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
})
// 存储窗口引用
dialogWindows.set(messageId, dialogWindow)
// 编码对话框数据
const encodedData = encodeURIComponent(JSON.stringify(dialogData))
// 加载对话框页面
const dialogUrl = `file://${path.join(__dirname, '../public/dialog.html')}?data=${encodedData}`
dialogWindow.loadURL(dialogUrl)
// 窗口准备好后显示并居中
dialogWindow.once('ready-to-show', () => {
// 计算居中位置
if (mainWindow && !mainWindow.isDestroyed()) {
const mainBounds = mainWindow.getBounds()
const dialogBounds = dialogWindow.getBounds()
const x = Math.round(mainBounds.x + (mainBounds.width - dialogBounds.width) / 2)
const y = Math.round(mainBounds.y + (mainBounds.height - dialogBounds.height) / 2)
dialogWindow.setPosition(x, y)
} else {
dialogWindow.center()
}
dialogWindow.show()
dialogWindow.focus()
})
// 窗口关闭时清理
dialogWindow.on('closed', () => {
dialogWindows.delete(messageId)
const callback = dialogCallbacks.get(messageId)
if (callback) {
dialogCallbacks.delete(messageId)
callback(false) // 默认返回 false (取消)
}
})
log.info(`对话框窗口已创建: ${messageId}`)
})
}
// 显示问题对话框
ipcMain.handle('show-question-dialog', async (_event, questionData) => {
log.info('收到显示对话框请求:', questionData)
try {
const result = await createQuestionDialog(questionData)
log.info(`对话框结果: ${result}`)
return result
} catch (error) {
log.error('创建对话框失败:', error)
return false
}
})
// 处理对话框响应
ipcMain.handle('dialog-response', async (_event, messageId: string, choice: boolean) => {
log.info(`收到对话框响应: ${messageId} = ${choice}`)
const callback = dialogCallbacks.get(messageId)
if (callback) {
dialogCallbacks.delete(messageId)
callback(choice)
}
// 关闭对话框窗口
const dialogWindow = dialogWindows.get(messageId)
if (dialogWindow && !dialogWindow.isDestroyed()) {
dialogWindow.close()
}
dialogWindows.delete(messageId)
return true
})
// 调整对话框窗口大小
ipcMain.handle('resize-dialog-window', async (_event, height: number) => {
// 获取当前活动的对话框窗口(最后创建的)
const dialogWindow = Array.from(dialogWindows.values()).pop()
if (dialogWindow && !dialogWindow.isDestroyed()) {
const bounds = dialogWindow.getBounds()
dialogWindow.setBounds({
...bounds,
height: Math.max(150, Math.min(400, height))
})
}
})
// Git相关
ipcMain.handle('download-git', async () => {
const appRoot = getAppRoot()
@@ -785,7 +919,7 @@ ipcMain.handle('check-git-update', async () => {
// 不执行fetch直接检查本地状态
// 这样避免了直接访问GitHub而是在后续的pull操作中使用镜像站
// 获取当前HEAD的commit hash
const currentCommit = await new Promise<string>((resolve, reject) => {
const revParseProc = spawn(gitPath, ['rev-parse', 'HEAD'], {
@@ -811,14 +945,13 @@ ipcMain.handle('check-git-update', async () => {
})
log.info(`当前本地commit: ${currentCommit}`)
// 由于我们跳过了fetch步骤避免直接访问GitHub
// 我们无法准确知道远程是否有更新
// 因此返回true让后续的pull操作通过镜像站来检查和获取更新
// 如果没有更新pull操作会很快完成且不会有实际变化
log.info('跳过远程检查返回hasUpdate=true以触发镜像站更新流程')
return { hasUpdate: true, skipReason: 'avoided_github_access' }
} catch (error) {
log.error('检查Git更新失败:', error)
// 如果检查失败返回true以触发更新流程确保代码是最新的
@@ -1084,23 +1217,23 @@ app.on('before-quit', async event => {
// 立即开始强制清理,不等待优雅关闭
log.info('开始强制清理所有相关进程')
try {
// 并行执行多种清理方法
const cleanupPromises = [
// 方法1: 使用我们的进程管理器
forceKillRelatedProcesses(),
// 方法2: 直接使用 taskkill 命令
new Promise<void>((resolve) => {
new Promise<void>(resolve => {
if (process.platform === 'win32') {
const appRoot = getAppRoot()
const commands = [
`taskkill /f /im python.exe`,
`wmic process where "CommandLine like '%main.py%'" delete`,
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`,
]
let completed = 0
commands.forEach(cmd => {
exec(cmd, () => {
@@ -1110,26 +1243,26 @@ app.on('before-quit', async event => {
}
})
})
// 2秒超时
setTimeout(resolve, 2000)
} else {
resolve()
}
})
}),
]
// 最多等待3秒
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000))
await Promise.race([Promise.all(cleanupPromises), timeoutPromise])
log.info('进程清理完成')
} catch (e) {
log.error('进程清理时出错:', e)
}
log.info('应用强制退出')
// 使用 process.exit 而不是 app.exit更加强制
setTimeout(() => {
process.exit(0)

View File

@@ -63,6 +63,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath),
// 对话框相关
showQuestionDialog: (questionData: any) => ipcRenderer.invoke('show-question-dialog', questionData),
dialogResponse: (messageId: string, choice: boolean) => ipcRenderer.invoke('dialog-response', messageId, choice),
resizeDialogWindow: (height: number) => ipcRenderer.invoke('resize-dialog-window', height),
// 监听下载进度
onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.on('download-progress', (_, progress) => callback(progress))

276
frontend/public/dialog.html Normal file
View File

@@ -0,0 +1,276 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>操作确认</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
background: #f5f5f5;
padding: 20px;
overflow: hidden;
}
.dialog-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-width: 100%;
height: auto;
display: flex;
flex-direction: column;
}
.dialog-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #e1e5e9;
background: #f8f9fa;
border-radius: 8px 8px 0 0;
}
.dialog-title {
font-size: 16px;
font-weight: 600;
color: #212529;
margin: 0;
}
.dialog-content {
padding: 24px;
flex: 1;
min-height: 60px;
}
.dialog-message {
font-size: 14px;
line-height: 1.6;
color: #495057;
margin: 0;
word-wrap: break-word;
white-space: pre-wrap;
}
.dialog-actions {
padding: 16px 24px 24px;
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #e1e5e9;
background: #f8f9fa;
border-radius: 0 0 8px 8px;
}
.dialog-button {
padding: 10px 20px;
border: 1px solid #dee2e6;
border-radius: 6px;
background: white;
color: #495057;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
min-width: 80px;
outline: none;
}
.dialog-button:hover {
background: #e9ecef;
border-color: #adb5bd;
}
.dialog-button:focus {
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
border-color: #86b7fe;
}
.dialog-button:active {
transform: translateY(1px);
}
.dialog-button.primary {
background: #0d6efd;
color: white;
border-color: #0d6efd;
}
.dialog-button.primary:hover {
background: #0b5ed7;
border-color: #0a58ca;
}
.dialog-button.danger {
background: #dc3545;
color: white;
border-color: #dc3545;
}
.dialog-button.danger:hover {
background: #c82333;
border-color: #bd2130;
}
/* 键盘导航样式 */
.dialog-button:focus-visible {
outline: 2px solid #0d6efd;
outline-offset: 2px;
}
/* 暗色主题支持 */
@media (prefers-color-scheme: dark) {
body {
background: #212529;
color: #f8f9fa;
}
.dialog-container {
background: #343a40;
}
.dialog-header,
.dialog-actions {
background: #2c3236;
border-color: #495057;
}
.dialog-title {
color: #f8f9fa;
}
.dialog-message {
color: #dee2e6;
}
.dialog-button {
background: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
.dialog-button:hover {
background: #5a6268;
border-color: #7c848a;
}
}
/* 动画效果 */
.dialog-container {
animation: dialogSlideIn 0.2s ease-out;
}
@keyframes dialogSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
</style>
</head>
<body>
<div class="dialog-container">
<div class="dialog-header">
<h3 class="dialog-title" id="dialog-title">操作确认</h3>
</div>
<div class="dialog-content">
<p class="dialog-message" id="dialog-message">是否要执行此操作?</p>
</div>
<div class="dialog-actions" id="dialog-actions">
<!-- 按钮将通过 JavaScript 动态生成 -->
</div>
</div>
<script>
// 获取传递的参数
const urlParams = new URLSearchParams(window.location.search);
const data = JSON.parse(decodeURIComponent(urlParams.get('data') || '{}'));
// 设置对话框内容
document.getElementById('dialog-title').textContent = data.title || '操作确认';
document.getElementById('dialog-message').textContent = data.message || '是否要执行此操作?';
// 创建按钮
const actionsContainer = document.getElementById('dialog-actions');
const options = data.options || ['确定', '取消'];
options.forEach((option, index) => {
const button = document.createElement('button');
button.className = 'dialog-button';
button.textContent = option;
// 第一个按钮设为主要按钮
if (index === 0) {
button.className += ' primary';
}
// 绑定点击事件
button.addEventListener('click', () => {
// 发送结果到主进程
if (window.electronAPI && window.electronAPI.dialogResponse) {
const choice = index === 0; // 第一个选项为 true
window.electronAPI.dialogResponse(data.messageId, choice);
}
// 不需要手动关闭窗口,主进程会处理
// 移除 window.electronAPI.windowClose() 调用
});
actionsContainer.appendChild(button);
});
// 自动聚焦第一个按钮
setTimeout(() => {
const firstButton = actionsContainer.querySelector('.dialog-button');
if (firstButton) {
firstButton.focus();
}
}, 100);
// 键盘事件处理
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
// ESC 键相当于取消
if (window.electronAPI && window.electronAPI.dialogResponse) {
window.electronAPI.dialogResponse(data.messageId, false);
}
// 不需要手动关闭窗口,主进程会处理
} else if (event.key === 'Enter') {
// Enter 键相当于确定
const focusedButton = document.activeElement;
if (focusedButton && focusedButton.classList.contains('dialog-button')) {
focusedButton.click();
} else {
// 如果没有聚焦按钮,默认点击第一个
const firstButton = actionsContainer.querySelector('.dialog-button');
if (firstButton) {
firstButton.click();
}
}
}
});
// 窗口加载完成后调整大小
window.addEventListener('load', () => {
if (window.electronAPI && window.electronAPI.resizeDialogWindow) {
// 获取内容高度
const container = document.querySelector('.dialog-container');
const height = container.offsetHeight + 40; // 加上一些边距
window.electronAPI.resizeDialogWindow(Math.min(height, 400));
}
});
</script>
</body>
</html>

View File

@@ -1,51 +1,26 @@
<template>
<div style="display: none">
<!-- 这是一个隐藏的监听组件不需要UI -->
</div>
<!-- 简单的自定义对话框 -->
<div v-if="showDialog" class="dialog-overlay" @click.self="showDialog = false">
<div class="dialog-container">
<div class="dialog-header">
<h3>{{ dialogData.title }}</h3>
</div>
<div class="dialog-content">
<p>{{ dialogData.message }}</p>
</div>
<div class="dialog-actions">
<button
v-for="(option, index) in dialogData.options"
:key="index"
class="dialog-button"
@click="handleChoice(index)"
>
{{ option }}
</button>
</div>
</div>
<!-- 现在使用系统级对话框窗口而不是应用内弹窗 -->
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
import { onMounted, onUnmounted } from 'vue'
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
import { logger } from '@/utils/logger'
// WebSocket hook
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
// 对话框状态
const showDialog = ref(false)
const dialogData = ref({
title: '',
message: '',
options: ['确定', '取消'],
messageId: ''
})
// 存储订阅ID用于取消订阅
let subscriptionId: string
// 检查是否在 Electron 环境中
const isElectron = () => {
return typeof window !== 'undefined' && (window as any).electronAPI
}
// 发送用户选择结果到后端
const sendResponse = (messageId: string, choice: boolean) => {
const response = {
@@ -59,39 +34,40 @@ const sendResponse = (messageId: string, choice: boolean) => {
sendRaw('Response', response)
}
// 处理用户选择
const handleChoice = (choiceIndex: number) => {
const choice = choiceIndex === 0 // 第一个选项为true其他为false
sendResponse(dialogData.value.messageId, choice)
showDialog.value = false
}
// 显示问题对话框
const showQuestion = (questionData: any) => {
// 显示系统级问题对话框
const showQuestion = async (questionData: any) => {
const title = questionData.title || '操作提示'
const message = questionData.message || ''
const options = questionData.options || ['确定', '取消']
const messageId = questionData.message_id || 'fallback_' + Date.now()
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
logger.info('[WebSocket消息监听器] 显示系统级对话框:', questionData)
// 设置对话框数据
dialogData.value = {
title,
message,
options,
messageId
if (!isElectron()) {
logger.error('[WebSocket消息监听器] 不在 Electron 环境中,无法显示系统级对话框')
// 在非 Electron 环境中,使用默认响应
sendResponse(messageId, false)
return
}
showDialog.value = true
// 在下一个tick自动聚焦第一个按钮
nextTick(() => {
const firstButton = document.querySelector('.dialog-button:first-child') as HTMLButtonElement
if (firstButton) {
firstButton.focus()
}
})
try {
// 调用 Electron API 显示系统级对话框
const result = await (window as any).electronAPI.showQuestionDialog({
title,
message,
options,
messageId
})
logger.info('[WebSocket消息监听器] 系统级对话框返回结果:', result)
// 发送结果到后端
sendResponse(messageId, result)
} catch (error) {
logger.error('[WebSocket消息监听器] 显示系统级对话框失败:', error)
// 出错时发送默认响应
sendResponse(messageId, false)
}
}
// 消息处理函数
@@ -137,14 +113,14 @@ const handleObjectMessage = (data: any) => {
logger.info('[WebSocket消息监听器] 发现Question类型消息')
if (data.message_id) {
logger.info('[WebSocket消息监听器] message_id存在显示选择弹窗')
logger.info('[WebSocket消息监听器] message_id存在显示系统级对话框')
showQuestion(data)
return
} else {
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
// 即使缺少message_id也尝试显示弹窗使用当前时间戳作为ID
// 即使缺少message_id也尝试显示对话框使用当前时间戳作为ID
const fallbackId = 'fallback_' + Date.now()
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
logger.info('[WebSocket消息监听器] 使用备用ID显示对话框:', fallbackId)
showQuestion({
...data,
message_id: fallbackId
@@ -210,150 +186,4 @@ onUnmounted(() => {
})
</script>
<style scoped>
/* 对话框遮罩层 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
/* 对话框容器 */
.dialog-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 300px;
max-width: 500px;
width: 90%;
animation: dialogAppear 0.2s ease-out;
}
/* 对话框头部 */
.dialog-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
/* 对话框内容 */
.dialog-content {
padding: 20px;
}
.dialog-content p {
margin: 0;
font-size: 14px;
line-height: 1.5;
color: #666;
word-break: break-word;
}
/* 按钮区域 */
.dialog-actions {
padding: 12px 20px 20px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 按钮样式 */
.dialog-button {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
color: #333;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
min-width: 60px;
}
.dialog-button:hover {
background: #f5f5f5;
border-color: #999;
}
.dialog-button:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
.dialog-button:first-child {
background: #007bff;
color: white;
border-color: #007bff;
}
.dialog-button:first-child:hover {
background: #0056b3;
border-color: #0056b3;
}
/* 出现动画 */
@keyframes dialogAppear {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
.dialog-container {
background: #2d2d2d;
color: #fff;
}
.dialog-header {
border-bottom-color: #444;
}
.dialog-header h3 {
color: #fff;
}
.dialog-content p {
color: #ccc;
}
.dialog-button {
background: #444;
color: #fff;
border-color: #555;
}
.dialog-button:hover {
background: #555;
border-color: #666;
}
.dialog-button:first-child {
background: #0d6efd;
border-color: #0d6efd;
}
.dialog-button:first-child:hover {
background: #0b5ed7;
border-color: #0b5ed7;
}
}
</style>

View File

@@ -1,64 +1,74 @@
export interface ElectronAPI {
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: any[]) => Promise<string[]>
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
// 窗口控制
windowMinimize: () => Promise<void>
windowMaximize: () => Promise<void>
windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean>
appQuit: () => Promise<void>
// 进程管理
getRelatedProcesses: () => Promise<any[]>
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
forceExit: () => Promise<{ success: boolean }>
// 初始化相关API
checkEnvironment: () => Promise<any>
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
downloadPython: (mirror?: string) => Promise<any>
installPip: () => Promise<any>
downloadGit: () => Promise<any>
installDependencies: (mirror?: string) => Promise<any>
cloneBackend: (repoUrl?: string) => Promise<any>
updateBackend: (repoUrl?: string) => Promise<any>
startBackend: () => Promise<{ success: boolean; error?: string }>
stopBackend?: () => Promise<{ success: boolean; error?: string }>
// 管理员权限相关
checkAdmin: () => Promise<boolean>
restartAsAdmin: () => Promise<void>
// 配置文件操作
saveConfig: (config: any) => Promise<void>
loadConfig: () => Promise<any>
resetConfig: () => Promise<void>
// 日志文件操作
getLogPath: () => Promise<string>
getLogFiles: () => Promise<string[]>
getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void>
// 保留原有方法以兼容现有代码
saveLogsToFile: (logs: string) => Promise<void>
loadLogsFromFile: () => Promise<string | null>
// 文件系统操作
openFile: (filePath: string) => Promise<void>
showItemInFolder: (filePath: string) => Promise<void>
// 监听下载进度
onDownloadProgress: (callback: (progress: any) => void) => void
removeDownloadProgressListener: () => void
}
declare global {
interface ElectronAPI {
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: any[]) => Promise<string[]>
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
// 窗口控制
windowMinimize: () => Promise<void>
windowMaximize: () => Promise<void>
windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean>
appQuit: () => Promise<void>
// 进程管理
getRelatedProcesses: () => Promise<any[]>
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
forceExit: () => Promise<{ success: boolean }>
// 初始化相关API
checkEnvironment: () => Promise<any>
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
downloadPython: (mirror?: string) => Promise<any>
installPip: () => Promise<any>
downloadGit: () => Promise<any>
installDependencies: (mirror?: string) => Promise<any>
cloneBackend: (repoUrl?: string) => Promise<any>
updateBackend: (repoUrl?: string) => Promise<any>
startBackend: () => Promise<{ success: boolean; error?: string }>
stopBackend?: () => Promise<{ success: boolean; error?: string }>
// 管理员权限相关
checkAdmin: () => Promise<boolean>
restartAsAdmin: () => Promise<void>
// 配置文件操作
saveConfig: (config: any) => Promise<void>
loadConfig: () => Promise<any>
resetConfig: () => Promise<void>
// 日志文件操作
getLogPath: () => Promise<string>
getLogFiles: () => Promise<string[]>
getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void>
// 保留原有方法以兼容现有代码
saveLogsToFile: (logs: string) => Promise<void>
loadLogsFromFile: () => Promise<string | null>
// 文件系统操作
openFile: (filePath: string) => Promise<void>
showItemInFolder: (filePath: string) => Promise<void>
// 对话框相关
showQuestionDialog: (questionData: {
title?: string
message?: string
options?: string[]
messageId?: string
}) => Promise<boolean>
dialogResponse: (messageId: string, choice: boolean) => Promise<boolean>
resizeDialogWindow: (height: number) => Promise<void>
// 监听下载进度
onDownloadProgress: (callback: (progress: any) => void) => void
removeDownloadProgressListener: () => void
}
interface Window {
electronAPI: ElectronAPI
}

View File

@@ -1,78 +0,0 @@
// Electron API 类型定义
export interface ElectronAPI {
// 开发工具
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>
// 窗口控制
windowMinimize: () => Promise<void>
windowMaximize: () => Promise<void>
windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean>
// 管理员权限检查
checkAdmin: () => Promise<boolean>
// 重启为管理员
restartAsAdmin: () => Promise<void>
appQuit: () => Promise<void>
// 进程管理
getRelatedProcesses: () => Promise<any[]>
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
forceExit: () => Promise<{ success: boolean }>
// 环境检查
checkEnvironment: () => Promise<{
pythonExists: boolean
gitExists: boolean
backendExists: boolean
dependenciesInstalled: boolean
isInitialized: boolean
}>
// 关键文件检查
checkCriticalFiles: () => Promise<{
pythonExists: boolean
pipExists: boolean
gitExists: boolean
mainPyExists: boolean
}>
// Python相关
downloadPython: (mirror: string) => Promise<{ success: boolean; error?: string }>
deletePython: () => Promise<{ success: boolean; error?: string }>
// pip相关
installPip: () => Promise<{ success: boolean; error?: string }>
deletePip: () => Promise<{ success: boolean; error?: string }>
// Git相关
downloadGit: () => Promise<{ success: boolean; error?: string }>
deleteGit: () => Promise<{ success: boolean; error?: string }>
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
// 后端代码相关
cloneBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
updateBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
// 依赖安装
installDependencies: (mirror: string) => Promise<{ success: boolean; error?: string }>
// 后端服务
startBackend: () => Promise<{ success: boolean; error?: string }>
// 下载进度监听
onDownloadProgress: (
callback: (progress: { progress: number; status: string; message: string }) => void
) => void
removeDownloadProgressListener: () => void
}
declare global {
interface Window {
electronAPI: ElectronAPI
}
}
export {}

View File

@@ -1,24 +1,13 @@
// 渲染进程日志工具
interface ElectronAPI {
getLogPath: () => Promise<string>
getLogFiles: () => Promise<string[]>
getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void>
}
const LogLevel = {
DEBUG: 'DEBUG',
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR'
} as const
declare global {
interface Window {
electronAPI: ElectronAPI
}
}
export enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR'
}
export type LogLevel = typeof LogLevel[keyof typeof LogLevel]
export { LogLevel }
class Logger {
// 直接使用原生console主进程会自动处理日志记录
@@ -40,32 +29,32 @@ class Logger {
// 获取日志文件路径
async getLogPath(): Promise<string> {
if (window.electronAPI) {
return await window.electronAPI.getLogPath()
if ((window as any).electronAPI) {
return await (window as any).electronAPI.getLogPath()
}
throw new Error('Electron API not available')
}
// 获取日志文件列表
async getLogFiles(): Promise<string[]> {
if (window.electronAPI) {
return await window.electronAPI.getLogFiles()
if ((window as any).electronAPI) {
return await (window as any).electronAPI.getLogFiles()
}
throw new Error('Electron API not available')
}
// 获取日志内容
async getLogs(lines?: number, fileName?: string): Promise<string> {
if (window.electronAPI) {
return await window.electronAPI.getLogs(lines, fileName)
if ((window as any).electronAPI) {
return await (window as any).electronAPI.getLogs(lines, fileName)
}
throw new Error('Electron API not available')
}
// 清空日志
async clearLogs(fileName?: string): Promise<void> {
if (window.electronAPI) {
await window.electronAPI.clearLogs(fileName)
if ((window as any).electronAPI) {
await (window as any).electronAPI.clearLogs(fileName)
console.info(`日志已清空: ${fileName || '当前文件'}`)
} else {
throw new Error('Electron API not available')
@@ -74,8 +63,8 @@ class Logger {
// 清理旧日志
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
if (window.electronAPI) {
await window.electronAPI.cleanOldLogs(daysToKeep)
if ((window as any).electronAPI) {
await (window as any).electronAPI.cleanOldLogs(daysToKeep)
console.info(`已清理${daysToKeep}天前的旧日志`)
} else {
throw new Error('Electron API not available')

225
websocket_test.html Normal file
View File

@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket消息测试</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.test-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
.status {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.status.disconnected {
background: #f8d7da;
color: #721c24;
}
.log {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: 10px;
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>WebSocket消息测试工具</h1>
<div class="test-section">
<h3>连接状态</h3>
<button onclick="connectWebSocket()">连接WebSocket</button>
<button onclick="disconnectWebSocket()">断开连接</button>
<div id="status" class="status disconnected">未连接</div>
</div>
<div class="test-section">
<h3>测试消息</h3>
<button onclick="sendQuestionMessage()">发送Question消息</button>
<button onclick="sendObjectMessage()">发送普通对象消息</button>
<button onclick="sendStringMessage()">发送字符串消息</button>
<button onclick="sendMalformedMessage()">发送格式错误消息</button>
</div>
<div class="test-section">
<h3>消息日志</h3>
<button onclick="clearLog()">清空日志</button>
<div id="log" class="log"></div>
</div>
</div>
<script>
let ws = null;
function log(message) {
const logDiv = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
logDiv.innerHTML += `[${timestamp}] ${message}\n`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function updateStatus(connected) {
const statusDiv = document.getElementById('status');
if (connected) {
statusDiv.textContent = 'WebSocket已连接';
statusDiv.className = 'status connected';
} else {
statusDiv.textContent = 'WebSocket未连接';
statusDiv.className = 'status disconnected';
}
}
function connectWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) {
log('WebSocket已经连接');
return;
}
ws = new WebSocket('ws://localhost:36163/api/core/ws');
ws.onopen = function() {
log('WebSocket连接已建立');
updateStatus(true);
};
ws.onmessage = function(event) {
log('收到消息: ' + event.data);
try {
const data = JSON.parse(event.data);
log('解析后的消息: ' + JSON.stringify(data, null, 2));
} catch (e) {
log('消息解析失败: ' + e.message);
}
};
ws.onclose = function() {
log('WebSocket连接已关闭');
updateStatus(false);
};
ws.onerror = function(error) {
log('WebSocket错误: ' + error);
updateStatus(false);
};
}
function disconnectWebSocket() {
if (ws) {
ws.close();
ws = null;
log('主动断开WebSocket连接');
updateStatus(false);
}
}
function sendMessage(message) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
log('错误: WebSocket未连接');
return;
}
const messageStr = JSON.stringify(message);
ws.send(messageStr);
log('发送消息: ' + messageStr);
}
function sendQuestionMessage() {
const message = {
id: "test_id_" + Date.now(),
type: "message",
data: {
type: "Question",
message_id: "q_" + Date.now(),
title: "测试问题",
message: "这是一个测试问题,请选择是否继续?"
}
};
sendMessage(message);
}
function sendObjectMessage() {
const message = {
id: "test_obj_" + Date.now(),
type: "message",
data: {
action: "test_action",
status: "running",
content: "这是一个普通的对象消息"
}
};
sendMessage(message);
}
function sendStringMessage() {
const message = {
id: "test_str_" + Date.now(),
type: "message",
data: "这是一个字符串消息"
};
sendMessage(message);
}
function sendMalformedMessage() {
const message = {
id: "test_malformed_" + Date.now(),
type: "message",
data: {
type: "Question",
// 缺少 message_id
title: "格式错误的问题",
message: "这个消息缺少message_id字段"
}
};
sendMessage(message);
}
function clearLog() {
document.getElementById('log').innerHTML = '';
}
// 页面加载时自动连接
window.onload = function() {
log('页面已加载准备测试WebSocket连接');
};
</script>
</body>
</html>