├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── README_EN.md
├── app
├── data
│ ├── scripts.json
│ ├── settings.json
│ └── tasks.json
├── main
│ ├── file-manager.js
│ ├── script-executor.js
│ ├── script-manager.js
│ ├── settings-manager.js
│ └── task-scheduler.js
└── renderer
│ ├── index.html
│ ├── renderer.js
│ ├── styles.css
│ └── task-manager.js
├── assets
├── icon-128.png
├── icon-16.png
├── icon-256.png
├── icon-32.png
├── icon-64.png
├── icon.ico
├── icon.png
└── icon.svg
├── build.bat
├── convert-icon.js
├── example_scripts
├── hello.js
├── hello.py
├── test.js
└── test.py
├── main.js
├── package-lock.json
├── package.json
└── preload.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 |
7 | # Build outputs
8 | dist/
9 | build/
10 | out/
11 |
12 | # Electron build artifacts
13 | *.exe
14 | *.dmg
15 | *.AppImage
16 | *.deb
17 | *.rpm
18 | *.snap
19 |
20 | # OS generated files
21 | .DS_Store
22 | .DS_Store?
23 | ._*
24 | .Spotlight-V100
25 | .Trashes
26 | ehthumbs.db
27 | Thumbs.db
28 | desktop.ini
29 |
30 | # IDE files
31 | .vscode/
32 | .idea/
33 | *.swp
34 | *.swo
35 | *~
36 |
37 | # Logs
38 | logs/
39 | *.log
40 |
41 | # Runtime data
42 | pids/
43 | *.pid
44 | *.seed
45 | *.pid.lock
46 |
47 | # Coverage directory used by tools like istanbul
48 | coverage/
49 |
50 | # nyc test coverage
51 | .nyc_output/
52 |
53 | # Dependency directories
54 | jspm_packages/
55 |
56 | # Optional npm cache directory
57 | .npm
58 |
59 | # Optional REPL history
60 | .node_repl_history
61 |
62 | # Output of 'npm pack'
63 | *.tgz
64 |
65 | # Yarn Integrity file
66 | .yarn-integrity
67 |
68 | # dotenv environment variables file
69 | .env
70 | .env.local
71 | .env.development.local
72 | .env.test.local
73 | .env.production.local
74 |
75 | # Electron user data (temporary directories)
76 | userData/
77 | temp/
78 |
79 | # Application specific
80 | app/data/scripts.json.backup
81 | app/data/tasks.json.backup
82 | *.tmp
83 | *.temp
84 |
85 | # Build configuration
86 | .npmrc
87 |
88 | # Development and testing files
89 | 定时任务*.md
90 | v*.md
91 | 测试*.md
92 |
93 | # Windows specific
94 | *.lnk
95 |
96 | # macOS specific
97 | .AppleDouble
98 | .LSOverride
99 |
100 | # Linux specific
101 | *~
102 |
103 | # Backup files
104 | *.bak
105 | *.backup
106 | *.old
107 |
108 | # Internal documentation (not for public repo)
109 | GITHUB_SETUP.md
110 | memory-bank/
111 | BUILD.md
112 | PORTABLE_FIX.md
113 | PORTABLE_NO_EXAMPLES.md
114 | macOS_compatibility_analysis.md
115 |
116 | # Analysis and development files
117 | *_analysis.md
118 | *_fix.md
119 | *_compatibility*.md
120 |
121 | # Additional ignored files
122 | .cursorignore
123 | example_scripts/test_warning.py
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 更新日志
2 |
3 | ## [1.3.9] - 2025-05-27
4 |
5 | ### 修复
6 | - 🔧 **Windows脚本启动优化**
7 | - 修复包含空格的脚本名称在Windows平台启动失败的问题
8 | - 移除start命令中的窗口标题参数,简化命令结构
9 | - 提升脚本启动的稳定性和兼容性
10 |
11 | ### 新增
12 | - 📧 **状态栏信息增强**
13 | - 在状态栏中间位置添加作者邮箱联系方式(windo@linux.do)
14 | - 新增可点击的GitHub仓库链接,方便用户访问项目主页
15 | - 优化状态栏布局,采用三栏设计(统计信息-联系信息-更新时间)
16 |
17 | ### 改进
18 | - 🎨 **用户界面优化**
19 | - 状态栏信息布局更加平衡和美观
20 | - GitHub图标使用官方SVG图标,视觉效果更专业
21 | - 鼠标悬停效果和点击反馈优化
22 |
23 | ### 技术细节
24 | - 修复script-executor.js中Windows平台的start命令参数
25 | - 在状态栏添加status-center区域支持三栏布局
26 | - 新增openExternal API支持打开外部链接
27 | - 完善CSS样式支持GitHub图标和邮箱链接
28 |
29 | ## [1.3.8] - 2025-05-27
30 |
31 | ### 修复
32 | - 🔧 **脚本删除时定时任务自动清理**
33 | - 修复脚本删除时定时任务未自动清理的数据一致性问题
34 | - 删除脚本时现在会自动清理所有相关的定时任务
35 | - 在删除确认对话框中显示将要删除的任务数量
36 | - 在删除成功通知中显示任务清理信息
37 | - 避免孤儿任务残留,提升数据一致性和用户体验
38 |
39 | ### 技术改进
40 | - 在TaskScheduler中新增`deleteTasksByScript(scriptId)`方法
41 | - 增强delete-script IPC处理器的任务清理逻辑
42 | - 改进删除脚本的用户界面反馈和信息提示
43 | - 完善脚本与定时任务的关联管理机制
44 |
45 | ## [1.3.7] - 2025-05-27
46 |
47 | ### 新增
48 | - 🔒 **单实例应用限制**
49 | - 确保同时只能运行一个脚本管理器实例
50 | - 尝试启动第二个实例时自动激活现有窗口
51 | - 跨平台支持,防止数据冲突和资源竞争
52 | - 📁 **右键菜单增强**
53 | - 新增"打开文件夹"功能,可在系统文件管理器中快速定位脚本文件
54 | - 跨平台支持:Windows(资源管理器)、macOS(Finder)、Linux(默认文件管理器)
55 | - 🐍 **Python脚本类型增强**
56 | - 新增 `.pyw` 文件扩展名支持
57 | - 将 `.pyw` 文件从"其他"类别归类为Python类型
58 | - Windows平台智能执行器选择:`.py`使用python.exe,`.pyw`使用pythonw.exe(无控制台窗口)
59 | - 跨平台兼容性:macOS/Linux上统一使用python命令执行.pyw文件
60 |
61 | ### 改进
62 | - 🛡️ **用户界面安全性**
63 | - 禁用模态对话框点击外部区域关闭功能,防止意外关闭重要对话框
64 | - 禁用点击脚本卡片直接启动功能,防止误触发脚本执行
65 | - 保留ESC键和关闭按钮的正常功能
66 | - � **脚本执行优化**
67 | - 新增 `getPythonExecutor()` 方法智能选择Python执行器
68 | - 优化GUI Python应用的执行体验(Windows上无控制台窗口干扰)
69 | - 完善文件类型检测和过滤器
70 | - 🎯 **用户体验提升**
71 | - 应用启动更加稳定,避免多实例冲突
72 | - 右键菜单操作更加便捷,支持快速访问脚本所在文件夹
73 | - Python开发者工作流程优化,正确处理GUI应用脚本
74 |
75 | ### 技术细节
76 | - 在 `main.js` 中添加单实例锁定逻辑和 `activateMainWindow()` 方法
77 | - 在 `main.js` 中添加 `open-script-folder` IPC处理器,使用 `shell.showItemInFolder()`
78 | - 在 `renderer.js` 中注释掉模态对话框外部点击关闭事件监听器
79 | - 在 `renderer.js` 中添加右键菜单"打开文件夹"事件处理和 `openScriptFolder()` 方法
80 | - 在 `file-manager.js` 中添加.pyw到支持扩展名和类型映射
81 | - 在 `renderer.js` 中更新FILE_EXTENSIONS和脚本文件检测
82 | - 在 `main.js` 中添加.pyw到Python脚本文件过滤器
83 | - 在 `script-executor.js` 中实现智能Python执行器选择逻辑
84 |
85 | ## [1.3.6] - 2025-05-27
86 |
87 | ### 改进
88 | - 📚 **文档优化**
89 | - 修改README.md,新增英文版 README_EN.md
90 |
91 | ## [1.3.5] - 2025-05-26
92 |
93 | ### 新增
94 | - 🍎 **macOS 兼容性全面增强**
95 | - 脚本执行器支持 macOS Terminal.app 集成
96 | - 使用 AppleScript 在新终端窗口中启动脚本
97 | - 添加 `.command` 和 `.tool` 文件类型支持
98 | - 托盘图标使用 macOS 模板图标模式
99 |
100 | ### 改进
101 | - 🔧 **跨平台脚本执行优化**
102 | - 统一工作目录处理,使用绝对路径
103 | - 平台特定的文件类型过滤
104 | - 文件对话框根据平台显示相关脚本类型
105 | - 📁 **文件管理增强**
106 | - 动态生成平台相关的文件过滤器
107 | - 改进文件类型映射和识别
108 |
109 | ### 技术细节
110 | - 在 `script-executor.js` 中添加 macOS 专用的脚本启动逻辑
111 | - 在 `file-manager.js` 中实现平台特定的文件类型支持
112 | - 在 `main.js` 中添加动态文件过滤器生成
113 | - 优化托盘图标在 macOS 上的显示效果
114 |
115 | ## [1.3.4] - 2025-05-26
116 |
117 | ### 新增
118 | - 🖥️ **最小化到托盘功能**
119 | - 新增系统托盘图标支持
120 | - 添加"关闭窗口时最小化到托盘"设置项
121 | - 托盘菜单支持快速恢复窗口和退出程序
122 |
123 | ### 改进
124 | - 🎛️ **设置页面优化**
125 | - 新增"窗口行为"设置分类
126 | - 优化设置项布局和说明文字
127 | - 添加托盘图标主题选择功能
128 |
129 | ### 技术细节
130 | - 在`main.js`中添加`createTrayIcon`方法实现托盘功能
131 | - 在`settings.html`中添加窗口行为设置项
132 | - 新增`trayManager.js`处理托盘相关逻辑
133 | - 更新`preload.js`暴露新的设置项API
134 |
135 | ## [1.3.3] - 2025-05-26
136 |
137 | ### 修复
138 | - 🛠️ **定时任务与脚本卡片联动修复**
139 | - 修复新建、编辑、删除、启用/禁用定时任务后,主界面脚本卡片定时信息不会自动刷新的问题
140 | - 现在所有定时任务相关操作后,脚本卡片会自动刷新,定时信息即时同步,无需手动刷新页面
141 |
142 | ### 改进
143 | - 🧩 **定时任务自动命名规则优化**
144 | - 未填写任务名称时,自动采用脚本文件名或"脚本X的定时任务"作为任务名称,提升易用性和一致性
145 | - 🎨 **脚本卡片内部布局优化**
146 | - 调整卡片内脚本名称、描述、类型标签、定时任务信息等元素的排布与间距
147 | - 兼容不同内容长度和屏幕尺寸,提升信息层次和美观度
148 |
149 | ### 技术细节
150 | - 在`task-manager.js`的`createTask`、`handleEditTaskSubmit`、`deleteTask`、`toggleTask`等方法中,操作成功后均调用`window.scriptManager.refreshScripts()`,实现主界面卡片的自动刷新
151 | - 优化了定时任务自动命名逻辑,避免无意义名称
152 | - 优化了卡片HTML结构和CSS样式,提升信息展示效果
153 |
154 | ## [1.3.2] - 2025-05-26
155 |
156 | ### 界面优化
157 | - 📏 **分类标签间距优化**
158 | - 减小标签之间的间距,使界面更紧凑
159 | - 调整标签内部填充,避免在小屏幕上显示不完整
160 | - 优化标签计数样式,使其更加紧凑
161 |
162 | ### 改进
163 | - 🪟 **自定义确认对话框**
164 | - 替换原生确认对话框为自定义模态对话框
165 | - 保持与应用程序整体UI风格的一致性
166 | - 支持文本换行和居中对齐,提升可读性
167 | - 📱 **响应式布局优化**
168 | - 改进小屏幕和移动设备上的显示效果
169 | - 优化模态对话框在小屏幕上的显示
170 | - 在小屏幕上使用垂直布局的按钮组
171 | - 👆 **触摸友好改进**
172 | - 增大模态对话框关闭按钮的可点击区域
173 | - 优化按钮间距,提升触摸设备易用性
174 | - 🔄 **脚本分类逻辑修复**
175 | - 统一脚本类型定义,添加静态常量
176 | - 修复文件类型识别与分类不一致问题
177 | - 添加类型不匹配时的用户提示
178 | - 防止重复添加相同路径的脚本
179 | - 确保所有UI分类与后端分类一致
180 |
181 | ### 技术细节
182 | - 添加`showConfirmDialog`方法替代原生`confirm()`
183 | - 为确认对话框添加专用CSS样式
184 | - 优化小屏幕下的网格布局
185 | - 更新.gitignore文件,排除不需要上传的文件
186 | - 添加`ScriptManager.SCRIPT_TYPES`、`FILE_EXTENSIONS`和`TYPE_DISPLAYS`静态常量
187 | - 简化`detectScriptType`和`getScriptTypeDisplay`方法
188 | - 重构`updateCategoryCounts`方法,支持所有脚本类型
189 | - 添加文件扩展名与类型匹配检查逻辑
190 |
191 | ## [1.3.1] - 2025-05-25
192 |
193 | ### 界面优化
194 | - 🌙 **深色主题全面优化**
195 | - 调整背景色为更暗的色调,减少夜间使用时的眼睛疲劳
196 | - 提高文本对比度,使文字在深色背景下更加清晰可见
197 | - 修复表单样式中的硬编码颜色,确保所有元素在深色模式下可见
198 |
199 | ### 改进
200 | - 🔔 **通知系统改进**
201 | - 将通知位置从右上角移动到右下角,避免遮挡设置按钮
202 | - 修复深色主题下通知文字看不清的问题
203 | - 优化通知动画效果,从底部向上滑入
204 | - 📱 **布局优化**
205 | - 修复窗口最大化时卡片分布问题,使卡片能够充分利用整个屏幕宽度
206 | - 调整卡片大小和间距,提供更好的视觉体验
207 | - 🎨 **图标系统升级**
208 | - 将emoji图标替换为专业的SVG图标
209 | - 为每种脚本类型提供独特的图标,更容易区分不同类型的脚本
210 | - 添加图标样式,确保在各种尺寸下显示正常
211 |
212 | ### 技术细节
213 | - 重构CSS变量,确保深色主题下的一致性
214 | - 优化SVG图标系统,提高可扩展性
215 | - 改进通知组件的定位和样式
216 |
217 | ## [1.3.0] - 2025-05-25
218 |
219 | ### 统一名称
220 | - 🔄 **统一项目名称为 "Scripts Manager"**
221 | - 将所有"脚本管理器"更新为"Scripts Manager"
222 | - 统一仓库名称、项目名称和产品名称
223 | - 更新可执行文件命名格式为"ScriptsManager-版本号"
224 |
225 | ### 改进
226 | - 🌐 **国际化支持增强**
227 | - 增加双语界面元素
228 | - 优化中英文混合显示
229 | - 📦 **简化包结构**
230 | - 更新包名为"scripts-manager"
231 | - 更新应用ID为"com.scriptsmanager.app"
232 |
233 | ### 技术细节
234 | - 更新所有品牌相关引用
235 | - 重构应用标识符
236 | - 优化构建配置
237 |
238 | ### 修复
239 | - 🐛 **修复面板卡片右键设置定时失效问题**
240 | - 修复renderer.js中的初始化代码,删除不存在的ScriptManagerApp引用
241 | - 修改task-manager.js中的showNotification方法,删除对不存在的window.app的引用
242 | - 在renderer.js中添加额外的错误检查和处理,确保模态框正确显示
243 |
244 | ## [1.2.4] - 2025-05-24
245 |
246 | ### 修复
247 | - 🐛 **移除便携版示例脚本**
248 | - 解决便携版示例脚本路径问题
249 | - 避免"脚本文件不存在"错误
250 | - 便携版启动时创建空的脚本列表
251 |
252 | ### 改进
253 | - ✨ **简化便携版体验**
254 | - 便携版不再包含示例脚本
255 | - 用户可以手动添加自己的脚本
256 | - 减少便携版文件大小
257 |
258 | ### 技术细节
259 | - 从打包配置中移除example_scripts目录
260 | - 修改ScriptManager创建空的初始脚本列表
261 | - 确保便携版启动稳定性
262 |
263 | ## [1.2.2] - 2025-05-24
264 |
265 | ### 修复
266 | - 🐛 **修复便携版定时任务功能完全失效问题**
267 | - 解决便携版中无法创建、编辑定时任务的问题
268 | - 修复"新建定时任务"报错问题
269 | - 🐛 **修复便携版数据持久化问题**
270 | - 解决数据每次重启丢失的问题
271 | - 从临时目录改为用户目录存储数据
272 | - 🐛 **修复打包后文件路径解析错误**
273 | - 修复`__dirname`在打包后指向只读路径的问题
274 | - 修复相对路径`../data/`无法正确解析的问题
275 |
276 | ### 改进
277 | - ✨ **改进数据存储架构**
278 | - 数据存储位置:`~/.script-manager/`
279 | - 脚本配置:`~/.script-manager/scripts.json`
280 | - 定时任务:`~/.script-manager/tasks.json`
281 | - 🔧 **优化打包配置**
282 | - 移除不必要的`extraResources`配置
283 | - 简化构建流程
284 | - 📚 **完善构建文档**
285 | - 新增`PORTABLE_FIX.md`便携版修复指南
286 | - 更新`BUILD.md`构建说明
287 |
288 | ### 技术细节
289 | - 修复ScriptManager和TaskScheduler的数据文件路径
290 | - 使用`app.getPath('userData')`获取正确的用户数据目录
291 | - 修复已弃用的`substr`方法,改用`substring`
292 | - 确保便携版真正"便携"且数据持久化
293 |
294 | ## [1.2.1] - 2025-05-24
295 |
296 | ### 修复
297 | - 修复了定时任务脚本执行失败问题(脚本文件不存在: undefined错误)
298 | - 修复了Windows平台定时任务启动命令错误
299 | - 修复了定时任务重复执行的无限循环问题
300 | - 修复了任务管理界面编辑按钮无响应问题
301 | - 确保定时任务与面板启动使用相同的启动逻辑
302 |
303 | ### 改进
304 | - 优化了TaskScheduler的参数传递机制
305 | - 改进了错误处理和用户反馈
306 | - 增强了任务状态的同步和持久化
307 | - 完善了任务编辑功能和表单验证
308 |
309 | ### 技术细节
310 | - 在TaskScheduler中添加getScriptData方法获取完整脚本信息
311 | - 修复executeTask方法的参数传递问题
312 | - 实现完整的editTask方法和编辑表单界面
313 | - 统一定时任务和面板启动的脚本执行方式
314 |
315 | ## [1.2.0] - 2025-05-24
316 |
317 | ### 新增
318 | - ⏰ **定时任务功能**: 轻量级任务调度器
319 | - 支持三种调度类型:间隔执行、每日定时、每周定时
320 | - 可视化任务管理界面
321 | - 任务创建、编辑、删除功能
322 | - 任务启用/禁用控制
323 | - 立即执行任务功能
324 | - 脚本卡片定时状态显示
325 | - 右键菜单设置定时功能
326 | - 任务状态实时更新
327 | - 数据持久化存储
328 |
329 | ### 技术实现
330 | - 新增TaskScheduler类实现任务调度逻辑
331 | - 新增TaskManager类管理任务界面
332 | - 集成IPC通信支持定时任务操作
333 | - 添加tasks.json数据文件存储任务信息
334 |
335 | ## [1.1.1] - 2025-05-24
336 |
337 | ### 修复
338 | - 修复了使用相对路径的Python脚本(如hello.py)启动后立即退出的问题
339 | - 改进了Windows平台下脚本启动时的路径解析逻辑
340 | - 确保新启动的控制台窗口使用正确的工作目录
341 |
342 | ### 技术细节
343 | - 在Windows平台使用绝对路径避免相对路径解析问题
344 | - 为`start`命令添加`/D`参数指定正确的工作目录
345 | - 统一了跨平台的路径处理方式
346 |
347 | ## [1.1.0] - 2024-12-01
348 |
349 | ### 新增
350 | - 重构为脚本启动器模式,在独立窗口中运行脚本
351 | - 现代化卡片网格布局界面
352 | - 智能分类和搜索功能
353 | - 改进的错误处理和稳定性
354 |
355 | ### 修复
356 | - 解决GPU缓存错误问题
357 | - 修复中文编码显示问题
358 | - 优化用户数据目录处理
359 |
360 | ## [1.0.0] - 2024-12-01
361 |
362 | ### 新增
363 | - 初始版本发布
364 | - 支持Python、JavaScript、TypeScript、批处理、PowerShell和Bash脚本
365 | - 图形化界面管理脚本
366 | - 脚本执行和输出监控功能
367 | - 脚本添加、编辑、删除功能
368 | - 支持本地脚本文件管理
369 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Scripts Manager Team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | **In addition, all modified and distributed versions of this software must include a clear and prominent attribution to the original repository, Scripts Manager (https://github.com/hmhm2022/scripts-manager), in the source code, documentation, and any other relevant materials.**
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scripts Manager
2 |
3 | 一个轻量级桌面脚本管理工具,基于 Electron 构建,用于管理、启动和定时执行各种类型的脚本(Python、JavaScript、TypeScript、Batch、PowerShell 等),支持类似青龙面板的定时任务调度功能。
4 |
5 | [](https://github.com/hmhm2022/scripts-manager)
6 | [](LICENSE)
7 | [](#系统要求)
8 |
9 | ## 功能特点
10 |
11 | - 🖥️ **原生桌面应用**: 基于 Electron,提供原生桌面体验
12 | - 🚀 **脚本启动器**: 点击即启动,脚本在独立窗口中运行
13 | - 📁 **多脚本类型支持**: Python、JavaScript、TypeScript、Batch、PowerShell、Bash
14 | - 🎨 **现代化界面**: 卡片网格布局,类似应用商店体验
15 | - 🔍 **智能搜索**: 支持按名称、类型、描述搜索脚本
16 | - 📂 **文件浏览**: 内置文件选择器,方便添加脚本
17 | - 🏷️ **分类管理**: 按脚本类型自动分类和过滤
18 | - ⏰ **定时任务**: 轻量级任务调度器,支持间隔、每日、每周执行
19 | - 🌍 **跨平台支持**: Windows、macOS、Linux 全平台兼容
20 | - 🚀 **绿色便携**: 支持打包为便携版可执行文件
21 | - 🌙 **深色主题**: 支持浅色/深色主题切换
22 | - 🔔 **系统托盘**: 最小化到系统托盘,后台运行
23 | - 📧 **联系方式**: 状态栏显示GitHub仓库链接和作者邮箱,方便反馈和支持
24 |
25 | ## 安装和运行
26 |
27 | ### 方式一:下载预编译版本(推荐)
28 |
29 | 1. 前往 [Releases](https://github.com/hmhm2022/scripts-manager/releases) 页面
30 | 2. 下载文件: - **Windows**: `ScriptsManager-1.3.9-portable.exe` (便携版)
31 | 3. 运行下载的文件即可使用
32 |
33 | ### 方式二:开发环境运行
34 |
35 | #### 前提条件
36 | - Node.js (v16+)
37 | - npm
38 |
39 | #### 步骤
40 |
41 | 1. **克隆项目**
42 | ```bash
43 | git clone https://github.com/hmhm2022/scripts-manager.git
44 | cd scripts-manager
45 | ```
46 |
47 | 2. **安装依赖**
48 | ```bash
49 | npm install
50 | ```
51 |
52 | 3. **启动应用**
53 | ```bash
54 | # 普通模式
55 | npm start
56 |
57 | # 开发模式(带开发者工具)
58 | npm run dev
59 | ```
60 |
61 | ### 打包分发
62 |
63 | ```bash
64 | # Windows 便携版
65 | npm run build-portable
66 |
67 | # Windows 安装程序
68 | npm run build-installer
69 |
70 | # macOS DMG
71 | npm run build-mac
72 |
73 | # Linux AppImage
74 | npm run build-linux
75 |
76 | # 构建所有平台
77 | npm run dist-all
78 | ```
79 |
80 | ## 使用说明
81 |
82 | ### 脚本管理
83 |
84 | - **添加脚本**: 点击顶部"+"按钮,填写脚本信息
85 | - **编辑脚本**: 右键点击脚本卡片选择"编辑"
86 | - **删除脚本**: 右键点击脚本卡片选择"删除"
87 | - **搜索脚本**: 使用顶部搜索框
88 | - **分类过滤**: 点击分类标签(全部、Python、JavaScript等)
89 |
90 | ### 定时任务
91 |
92 | - **创建定时任务**: 点击顶部⏰按钮,选择"新建任务"
93 | - **管理任务**: 在任务管理界面中编辑、启用/禁用、删除任务
94 | - **快速设置**: 右键点击脚本卡片选择"设置定时"
95 | - **立即执行**: 在任务列表中点击"立即执行"按钮
96 |
97 | ### 脚本启动
98 |
99 | 1. 点击脚本卡片上的启动按钮(▶)或右键选择"启动脚本"
100 | 2. 脚本将在新的控制台窗口中启动
101 | 3. 脚本独立运行,可以关闭管理器应用
102 |
103 | ### 支持的脚本类型
104 |
105 | | 脚本类型 | 扩展名 | Windows | macOS | Linux | 运行环境要求 |
106 | |---------|--------|---------|-------|-------|-------------|
107 | | **Python** | `.py`, `.pyw` | ✅ | ✅ | ✅ | Python 3.x |
108 | | **JavaScript** | `.js` | ✅ | ✅ | ✅ | Node.js |
109 | | **TypeScript** | `.ts` | ✅ | ✅ | ✅ | ts-node |
110 | | **Batch** | `.bat`, `.cmd` | ✅ | ❌ | ❌ | Windows 内置 |
111 | | **PowerShell** | `.ps1` | ✅ | ✅ | ✅ | PowerShell Core |
112 | | **Bash** | `.sh` | ✅* | ✅ | ✅ | Bash Shell |
113 | | **macOS 脚本** | `.command`, `.tool` | ❌ | ✅ | ❌ | macOS 内置 |
114 |
115 | > *Windows 上的 Bash 脚本需要 WSL、Git Bash 或 Cygwin 环境
116 |
117 | ### 平台特性
118 |
119 | #### Windows
120 | - 脚本在新的 CMD 窗口中启动
121 | - 支持 Batch 和 PowerShell 脚本
122 | - 便携版无需安装,绿色运行
123 |
124 | #### macOS
125 | - 脚本在 Terminal.app 中启动
126 | - 支持 `.command` 和 `.tool` 文件
127 | - 原生 DMG 安装包
128 | - 支持 Intel 和 Apple Silicon
129 |
130 | #### Linux
131 | - 脚本在终端模拟器中启动
132 | - AppImage 格式,无需安装
133 | - 支持大多数现代发行版
134 |
135 | ## 技术架构
136 |
137 | ### 核心技术栈
138 |
139 | - **Electron**: 跨平台桌面应用框架
140 | - **Node.js**: 后端运行时
141 | - **原生JavaScript**: 前端界面
142 | - **JSON**: 数据存储
143 |
144 | ### 架构设计
145 |
146 | - **主进程**: 应用生命周期管理、IPC通信、系统API访问
147 | - **渲染进程**: 用户界面、用户交互
148 | - **IPC通信**: 主进程与渲染进程间的安全通信
149 | - **模块化设计**: 脚本管理、启动、文件操作分离
150 |
151 | ## 特色功能
152 |
153 | ### 🔒 安全性
154 | - 上下文隔离和预加载脚本确保安全
155 | - 禁用Node.js集成,防止安全漏洞
156 |
157 | ### 🌍 国际化
158 | - 完全支持中文路径和文件名
159 | - 正确处理中文脚本输出(GBK/UTF-8编码)
160 |
161 | ### ⚡ 性能优化
162 | - 禁用GPU加速,避免兼容性问题
163 | - 智能缓存管理
164 | - 异步操作,界面响应流畅
165 |
166 | ### 🎯 用户体验
167 | - 现代化卡片网格布局
168 | - 脚本在独立窗口中运行
169 | - 响应式界面设计
170 | - 右键菜单操作
171 |
172 | ## 故障排除
173 |
174 | ### 常见问题
175 |
176 | 1. **脚本启动失败**
177 | - 检查脚本路径是否正确
178 | - 确认相应运行时环境已安装
179 | - 查看控制台错误信息
180 |
181 | 2. **中文显示问题**
182 | - 应用已优化中文支持
183 | - 如有问题请检查系统编码设置
184 |
185 | 3. **权限问题**
186 | - 确保脚本文件有执行权限
187 | - PowerShell脚本可能需要调整执行策略
188 |
189 |
190 | 详细更新日志请查看 [CHANGELOG.md](CHANGELOG.md)
191 |
192 | ## 鸣谢
193 |
194 | 作为一个自然语言程序员,感谢以下工具和平台对本项目开发的巨大贡献:
195 |
196 | - **[Cursor](https://cursor.sh/)** - AI驱动的代码编辑器,提供了强大的AI编程辅助功能,大大提升了开发效率
197 | - **[Augment](https://augmentcode.com/)** - 智能代码助手平台,为项目开发提供了优秀的AI编程支持和代码优化建议
198 |
199 | 这些先进的AI工具让Scripts Manager的开发过程更加高效和智能化。
200 |
201 | ## 许可证
202 |
203 | 本项目采用 MIT License 许可证,并包含以下附加条款:
204 |
205 | ### MIT License + 附加条款
206 |
207 | **基础许可**: MIT License
208 |
209 | **附加要求**: 所有修改和分发的软件版本必须在源代码、文档和其他相关材料中包含对原始仓库的清晰和显著的归属声明:
210 |
211 | > **Scripts Manager** - https://github.com/hmhm2022/scripts-manager
212 |
213 | 这意味着如果您:
214 | - 修改本软件并重新分发
215 | - 基于本软件创建衍生作品
216 | - 在其他项目中使用本软件的代码
217 |
218 | 您需要在相关材料中明确标注本项目的来源和链接。
219 |
220 | 完整许可证条款请查看 [LICENSE](LICENSE) 文件。
221 |
222 | ---
223 |
224 | **享受 Scripts Manager 带来的高效脚本管理体验!** 🚀
225 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # Scripts Manager
2 |
3 | A lightweight desktop script management tool built with Electron for managing, launching, and scheduling various types of scripts (Python, JavaScript, TypeScript, Batch, PowerShell, etc.), featuring scheduled task functionality similar to Qinglong Panel.
4 |
5 | [](https://github.com/hmhm2022/scripts-manager)
6 | [](LICENSE)
7 | [](#system-requirements)
8 |
9 | ## Features
10 |
11 | - 🖥️ **Native Desktop App**: Built with Electron, providing native desktop experience
12 | - 🚀 **Script Launcher**: One-click launch, scripts run in independent windows
13 | - 📁 **Multi-Script Support**: Python, JavaScript, TypeScript, Batch, PowerShell, Bash
14 | - 🎨 **Modern Interface**: Card grid layout with app store-like experience
15 | - 🔍 **Smart Search**: Search scripts by name, type, and description
16 | - 📂 **File Browser**: Built-in file picker for easy script addition
17 | - 🏷️ **Category Management**: Automatic categorization and filtering by script type
18 | - ⏰ **Scheduled Tasks**: Lightweight task scheduler supporting interval, daily, and weekly execution
19 | - 🌍 **Cross-Platform**: Full compatibility with Windows, macOS, and Linux
20 | - 🚀 **Portable**: Support for portable executable packaging
21 | - 🌙 **Dark Theme**: Light/dark theme switching support
22 | - 🔔 **System Tray**: Minimize to system tray for background operation
23 | - 📧 **Contact Info**: Status bar displays GitHub repository link and author email for feedback and support
24 |
25 | ## Installation and Usage
26 |
27 | ### Option 1: Download Pre-built Releases (Recommended)
28 |
29 | 1. Go to [Releases](https://github.com/hmhm2022/scripts-manager/releases) page
30 | 2. Download file: - **Windows**: `ScriptsManager-1.3.9-portable.exe` (Portable)
31 | 3. Run the downloaded file to start using
32 |
33 | ### Option 2: Development Environment
34 |
35 | #### Prerequisites
36 | - Node.js (v16+)
37 | - npm
38 |
39 | #### Steps
40 |
41 | 1. **Clone the project**
42 | ```bash
43 | git clone https://github.com/hmhm2022/scripts-manager.git
44 | cd scripts-manager
45 | ```
46 |
47 | 2. **Install dependencies**
48 | ```bash
49 | npm install
50 | ```
51 |
52 | 3. **Start the application**
53 | ```bash
54 | # Normal mode
55 | npm start
56 |
57 | # Development mode (with dev tools)
58 | npm run dev
59 | ```
60 |
61 | ### Building and Distribution
62 |
63 | ```bash
64 | # Windows portable
65 | npm run build-portable
66 |
67 | # Windows installer
68 | npm run build-installer
69 |
70 | # macOS DMG
71 | npm run build-mac
72 |
73 | # Linux AppImage
74 | npm run build-linux
75 |
76 | # Build all platforms
77 | npm run dist-all
78 | ```
79 |
80 | ## User Guide
81 |
82 | ### Script Management
83 |
84 | - **Add Script**: Click the "+" button at the top and fill in script information
85 | - **Edit Script**: Right-click on script card and select "Edit"
86 | - **Delete Script**: Right-click on script card and select "Delete"
87 | - **Search Scripts**: Use the search box at the top
88 | - **Category Filter**: Click category tags (All, Python, JavaScript, etc.)
89 |
90 | ### Scheduled Tasks
91 |
92 | - **Create Scheduled Task**: Click the ⏰ button at the top and select "New Task"
93 | - **Manage Tasks**: Edit, enable/disable, delete tasks in the task management interface
94 | - **Quick Setup**: Right-click on script card and select "Set Schedule"
95 | - **Execute Immediately**: Click "Execute Now" button in the task list
96 |
97 | ### Script Execution
98 |
99 | 1. Click the launch button (▶) on script card or right-click and select "Launch Script"
100 | 2. Script will launch in a new console window
101 | 3. Scripts run independently, you can close the manager application
102 |
103 | ## Supported Script Types
104 |
105 | | Script Type | Extensions | Windows | macOS | Linux | Runtime Requirements |
106 | |-------------|------------|---------|-------|-------|---------------------|
107 | | **Python** | `.py`, `.pyw` | ✅ | ✅ | ✅ | Python 3.x |
108 | | **JavaScript** | `.js` | ✅ | ✅ | ✅ | Node.js |
109 | | **TypeScript** | `.ts` | ✅ | ✅ | ✅ | ts-node |
110 | | **Batch** | `.bat`, `.cmd` | ✅ | ❌ | ❌ | Windows Built-in |
111 | | **PowerShell** | `.ps1` | ✅ | ✅ | ✅ | PowerShell Core |
112 | | **Bash** | `.sh` | ✅* | ✅ | ✅ | Bash Shell |
113 | | **macOS Scripts** | `.command`, `.tool` | ❌ | ✅ | ❌ | macOS Built-in |
114 |
115 | > *Bash scripts on Windows require WSL, Git Bash, or Cygwin environment
116 |
117 | ### Platform Features
118 |
119 | #### Windows
120 | - Scripts launch in new CMD windows
121 | - Support for Batch and PowerShell scripts
122 | - Portable version requires no installation, runs green
123 |
124 | #### macOS
125 | - Scripts launch in Terminal.app
126 | - Support for `.command` and `.tool` files
127 | - Native DMG installer package
128 | - Support for Intel and Apple Silicon
129 |
130 | #### Linux
131 | - Scripts launch in terminal emulator
132 | - AppImage format, no installation required
133 | - Support for most modern distributions
134 |
135 | ## Technical Architecture
136 |
137 | ### Core Technology Stack
138 |
139 | - **Electron**: Cross-platform desktop application framework
140 | - **Node.js**: Backend runtime
141 | - **Vanilla JavaScript**: Frontend interface
142 | - **JSON**: Data storage
143 |
144 | ### Architecture Design
145 |
146 | - **Main Process**: Application lifecycle management, IPC communication, system API access
147 | - **Renderer Process**: User interface, user interaction
148 | - **IPC Communication**: Secure communication between main and renderer processes
149 | - **Modular Design**: Separation of script management, execution, and file operations
150 |
151 | ## Key Features
152 |
153 | ### 🔒 Security
154 | - Context isolation and preload scripts ensure security
155 | - Disabled Node.js integration prevents security vulnerabilities
156 |
157 | ### 🌍 Internationalization
158 | - Full support for Chinese paths and filenames
159 | - Proper handling of Chinese script output (GBK/UTF-8 encoding)
160 |
161 | ### ⚡ Performance Optimization
162 | - Disabled GPU acceleration to avoid compatibility issues
163 | - Smart cache management
164 | - Asynchronous operations for smooth interface responsiveness
165 |
166 | ### 🎯 User Experience
167 | - Modern card grid layout
168 | - Scripts run in independent windows
169 | - Responsive interface design
170 | - Right-click menu operations
171 |
172 | ## Troubleshooting
173 |
174 | ### Common Issues
175 |
176 | 1. **Script Launch Failure**
177 | - Check if script path is correct
178 | - Confirm the corresponding runtime environment is installed
179 | - Check console error messages
180 |
181 | 2. **Chinese Display Issues**
182 | - Application is optimized for Chinese support
183 | - If issues persist, check system encoding settings
184 |
185 | 3. **Permission Issues**
186 | - Ensure script files have execute permissions
187 | - PowerShell scripts may need execution policy adjustment
188 |
189 |
190 | For detailed changelog, see [CHANGELOG.md](CHANGELOG.md)
191 |
192 | ## Acknowledgments
193 |
194 | As a natural language coder, I would like to thank the following tools and platforms for their tremendous contributions to the development of this project:
195 |
196 | - **[Cursor](https://cursor.sh/)** - AI-powered code editor that provides powerful AI programming assistance, greatly improving development efficiency
197 | - **[Augment](https://augmentcode.com/)** - Intelligent code assistant platform that provides excellent AI programming support and code optimization suggestions for project development
198 |
199 | These advanced AI tools have made the development process of Scripts Manager more efficient and intelligent.
200 |
201 | ## License
202 |
203 | This project is licensed under the MIT License with the following additional terms:
204 |
205 | ### MIT License + Additional Terms
206 |
207 | **Base License**: MIT License
208 |
209 | **Additional Requirement**: All modified and distributed versions of this software must include a clear and prominent attribution to the original repository in the source code, documentation, and any other relevant materials:
210 |
211 | > **Scripts Manager** - https://github.com/hmhm2022/scripts-manager
212 |
213 | This means if you:
214 | - Modify this software and redistribute it
215 | - Create derivative works based on this software
216 | - Use code from this software in other projects
217 |
218 | You must clearly indicate the source and link to this project in the relevant materials.
219 |
220 | For the complete license terms, please see the [LICENSE](LICENSE) file.
221 |
222 | ---
223 |
224 | **Enjoy the efficient script management experience with Scripts Manager!** 🚀
225 |
--------------------------------------------------------------------------------
/app/data/scripts.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "示例Python脚本",
5 | "type": "python",
6 | "path": "./example_scripts/hello.py",
7 | "description": "这是一个示例Python脚本,演示基本的输出功能",
8 | "updatedAt": "2025-05-24T12:51:02.564Z"
9 | },
10 | {
11 | "id": 2,
12 | "name": "示例JavaScript脚本",
13 | "type": "javascript",
14 | "path": "./example_scripts/hello.js",
15 | "description": "这是一个示例JavaScript脚本,演示基本的输出功能",
16 | "updatedAt": "2025-05-24T12:50:59.057Z"
17 | },
18 | {
19 | "id": 3,
20 | "name": "空货位",
21 | "type": "python",
22 | "path": "C:\\Users\\储运部\\Desktop\\空货位.py",
23 | "description": "",
24 | "createdAt": "2025-05-24T07:42:13.169Z",
25 | "updatedAt": "2025-05-24T13:21:06.382Z"
26 | },
27 | {
28 | "id": 4,
29 | "name": "历史单据打印",
30 | "type": "python",
31 | "path": "C:\\Users\\储运部\\Desktop\\WMS 历史拣货单据打印.py",
32 | "description": "",
33 | "createdAt": "2025-05-24T07:42:46.057Z",
34 | "updatedAt": "2025-05-24T13:40:10.917Z"
35 | },
36 | {
37 | "id": 5,
38 | "name": "客户地址查询",
39 | "type": "python",
40 | "path": "C:\\Users\\储运部\\Desktop\\客户地址查询.py",
41 | "description": "",
42 | "createdAt": "2025-05-24T08:17:37.212Z",
43 | "updatedAt": "2025-05-24T13:21:09.664Z"
44 | }
45 | ]
--------------------------------------------------------------------------------
/app/data/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "pythonPath": "",
3 | "nodePath": "",
4 | "tsNodePath": "",
5 | "firstRun": true,
6 | "theme": "light",
7 | "language": "zh-CN",
8 | "autoCleanup": true,
9 | "maxProcesses": 10,
10 | "updatedAt": "2025-05-23T08:27:53.611Z"
11 | }
--------------------------------------------------------------------------------
/app/data/tasks.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "task_1748089021323_kc7bbi9rz",
4 | "scriptId": 4,
5 | "name": "历史单据打印",
6 | "schedule": {
7 | "type": "daily",
8 | "time": "21:28"
9 | },
10 | "enabled": true,
11 | "createdAt": "2025-05-24T12:17:01.323Z",
12 | "lastRun": "2025-05-24T15:25:12.150Z",
13 | "nextRun": "2025-05-25T13:28:00.000Z",
14 | "runCount": 17,
15 | "updatedAt": "2025-05-24T13:27:33.834Z"
16 | },
17 | {
18 | "id": "task_1748089208629_k7pyk45y1",
19 | "scriptId": 5,
20 | "name": "客户地址查询",
21 | "schedule": {
22 | "type": "daily",
23 | "time": "21:29"
24 | },
25 | "enabled": true,
26 | "createdAt": "2025-05-24T12:20:08.629Z",
27 | "lastRun": "2025-05-24T13:29:00.011Z",
28 | "nextRun": "2025-05-25T13:29:00.000Z",
29 | "runCount": 11,
30 | "updatedAt": "2025-05-24T13:28:38.095Z"
31 | },
32 | {
33 | "id": "task_1748093491776_mkustn8du",
34 | "scriptId": 1,
35 | "name": "示例Python",
36 | "schedule": {
37 | "type": "daily",
38 | "time": "09:00"
39 | },
40 | "enabled": false,
41 | "createdAt": "2025-05-24T13:31:31.776Z",
42 | "lastRun": "2025-05-24T13:31:35.361Z",
43 | "nextRun": "2025-05-25T01:00:00.000Z",
44 | "runCount": 1,
45 | "updatedAt": "2025-05-24T13:40:35.193Z"
46 | },
47 | {
48 | "id": "task_1748100342127_ihcr6v1gf",
49 | "scriptId": 3,
50 | "name": "空货位",
51 | "schedule": {
52 | "type": "daily",
53 | "time": "09:00"
54 | },
55 | "enabled": true,
56 | "createdAt": "2025-05-24T15:25:42.127Z",
57 | "lastRun": "2025-05-24T15:27:14.123Z",
58 | "nextRun": "2025-05-25T01:00:00.000Z",
59 | "runCount": 2,
60 | "updatedAt": "2025-05-24T15:27:09.785Z"
61 | }
62 | ]
--------------------------------------------------------------------------------
/app/main/file-manager.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs').promises;
2 | const path = require('path');
3 |
4 | class FileManager {
5 | constructor() {
6 | // 基础支持的脚本类型
7 | this.baseSupportedExtensions = ['.py', '.pyw', '.js', '.ts', '.sh'];
8 | // 根据平台设置支持的扩展名
9 | this.supportedExtensions = this.getSupportedExtensionsForPlatform();
10 | }
11 |
12 | getSupportedExtensionsForPlatform() {
13 | const base = [...this.baseSupportedExtensions];
14 |
15 | if (process.platform === 'win32') {
16 | // Windows 平台添加特有的脚本类型
17 | return [...base, '.bat', '.cmd', '.ps1'];
18 | } else if (process.platform === 'darwin') {
19 | // macOS 平台添加特有的脚本类型
20 | return [...base, '.command', '.tool'];
21 | } else {
22 | // Linux 和其他平台
23 | return base;
24 | }
25 | }
26 |
27 | async validateFile(filePath) {
28 | try {
29 | // 检查文件是否存在
30 | await fs.access(filePath);
31 |
32 | // 获取文件信息
33 | const stats = await fs.stat(filePath);
34 |
35 | if (!stats.isFile()) {
36 | return { success: false, error: '指定路径不是文件' };
37 | }
38 |
39 | // 获取文件扩展名和类型
40 | const ext = path.extname(filePath).toLowerCase();
41 | const scriptType = this.getScriptTypeByExtension(ext);
42 |
43 | // 获取文件大小
44 | const fileSize = stats.size;
45 |
46 | // 检查文件是否过大(超过10MB)
47 | if (fileSize > 10 * 1024 * 1024) {
48 | return {
49 | success: false,
50 | error: '文件过大(超过10MB),可能不是脚本文件'
51 | };
52 | }
53 |
54 | return {
55 | success: true,
56 | fileInfo: {
57 | path: filePath,
58 | name: path.basename(filePath),
59 | extension: ext,
60 | scriptType: scriptType,
61 | size: fileSize,
62 | lastModified: stats.mtime,
63 | isSupported: this.supportedExtensions.includes(ext)
64 | }
65 | };
66 |
67 | } catch (error) {
68 | return {
69 | success: false,
70 | error: `文件验证失败: ${error.message}`
71 | };
72 | }
73 | }
74 |
75 | getScriptTypeByExtension(extension) {
76 | const typeMap = {
77 | '.py': 'python',
78 | '.pyw': 'python',
79 | '.js': 'javascript',
80 | '.ts': 'typescript',
81 | '.bat': 'batch',
82 | '.cmd': 'batch',
83 | '.ps1': 'powershell',
84 | '.sh': 'bash',
85 | '.command': 'bash', // macOS 可执行脚本
86 | '.tool': 'bash' // macOS 工具脚本
87 | };
88 |
89 | return typeMap[extension.toLowerCase()] || 'other';
90 | }
91 |
92 | async browseDirectory(dirPath) {
93 | try {
94 | const items = await fs.readdir(dirPath, { withFileTypes: true });
95 |
96 | const result = await Promise.all(
97 | items.map(async (item) => {
98 | const fullPath = path.join(dirPath, item.name);
99 | const stats = await fs.stat(fullPath);
100 |
101 | return {
102 | name: item.name,
103 | path: fullPath,
104 | isDirectory: item.isDirectory(),
105 | isFile: item.isFile(),
106 | size: stats.size,
107 | lastModified: stats.mtime,
108 | extension: item.isFile() ? path.extname(item.name).toLowerCase() : null,
109 | isScriptFile: item.isFile() && this.supportedExtensions.includes(
110 | path.extname(item.name).toLowerCase()
111 | )
112 | };
113 | })
114 | );
115 |
116 | // 排序:目录在前,然后按名称排序
117 | result.sort((a, b) => {
118 | if (a.isDirectory && !b.isDirectory) return -1;
119 | if (!a.isDirectory && b.isDirectory) return 1;
120 | return a.name.localeCompare(b.name);
121 | });
122 |
123 | return {
124 | success: true,
125 | currentPath: dirPath,
126 | items: result
127 | };
128 |
129 | } catch (error) {
130 | return {
131 | success: false,
132 | error: `浏览目录失败: ${error.message}`
133 | };
134 | }
135 | }
136 |
137 | async getRecentFiles(limit = 10) {
138 | try {
139 | // 这里可以实现最近使用文件的逻辑
140 | // 暂时返回空数组,后续可以扩展
141 | return {
142 | success: true,
143 | files: []
144 | };
145 | } catch (error) {
146 | return {
147 | success: false,
148 | error: error.message
149 | };
150 | }
151 | }
152 |
153 | async searchFiles(searchPath, pattern, options = {}) {
154 | try {
155 | const {
156 | recursive = true,
157 | includeHidden = false,
158 | fileTypesOnly = this.supportedExtensions
159 | } = options;
160 |
161 | const results = [];
162 |
163 | await this.searchFilesRecursive(
164 | searchPath,
165 | pattern,
166 | results,
167 | recursive,
168 | includeHidden,
169 | fileTypesOnly
170 | );
171 |
172 | return {
173 | success: true,
174 | files: results
175 | };
176 |
177 | } catch (error) {
178 | return {
179 | success: false,
180 | error: `搜索文件失败: ${error.message}`
181 | };
182 | }
183 | }
184 |
185 | async searchFilesRecursive(dirPath, pattern, results, recursive, includeHidden, fileTypes) {
186 | try {
187 | const items = await fs.readdir(dirPath, { withFileTypes: true });
188 |
189 | for (const item of items) {
190 | // 跳过隐藏文件(如果不包含隐藏文件)
191 | if (!includeHidden && item.name.startsWith('.')) {
192 | continue;
193 | }
194 |
195 | const fullPath = path.join(dirPath, item.name);
196 |
197 | if (item.isFile()) {
198 | const ext = path.extname(item.name).toLowerCase();
199 |
200 | // 检查文件类型
201 | if (fileTypes.length > 0 && !fileTypes.includes(ext)) {
202 | continue;
203 | }
204 |
205 | // 检查文件名是否匹配模式
206 | if (item.name.toLowerCase().includes(pattern.toLowerCase())) {
207 | const stats = await fs.stat(fullPath);
208 | results.push({
209 | name: item.name,
210 | path: fullPath,
211 | extension: ext,
212 | scriptType: this.getScriptTypeByExtension(ext),
213 | size: stats.size,
214 | lastModified: stats.mtime
215 | });
216 | }
217 | } else if (item.isDirectory() && recursive) {
218 | // 递归搜索子目录
219 | await this.searchFilesRecursive(
220 | fullPath,
221 | pattern,
222 | results,
223 | recursive,
224 | includeHidden,
225 | fileTypes
226 | );
227 | }
228 | }
229 | } catch (error) {
230 | // 忽略无法访问的目录
231 | console.warn(`无法访问目录: ${dirPath}, 错误: ${error.message}`);
232 | }
233 | }
234 |
235 | async createBackup(filePath) {
236 | try {
237 | const backupPath = `${filePath}.backup.${Date.now()}`;
238 | await fs.copyFile(filePath, backupPath);
239 |
240 | return {
241 | success: true,
242 | backupPath: backupPath
243 | };
244 | } catch (error) {
245 | return {
246 | success: false,
247 | error: `创建备份失败: ${error.message}`
248 | };
249 | }
250 | }
251 |
252 | normalizePath(inputPath) {
253 | // 标准化路径,处理相对路径和绝对路径
254 | if (path.isAbsolute(inputPath)) {
255 | return inputPath;
256 | }
257 |
258 | return path.resolve(process.cwd(), inputPath);
259 | }
260 |
261 | isValidScriptFile(filePath) {
262 | const ext = path.extname(filePath).toLowerCase();
263 | return this.supportedExtensions.includes(ext);
264 | }
265 |
266 | getSupportedExtensions() {
267 | return [...this.supportedExtensions];
268 | }
269 | }
270 |
271 | module.exports = FileManager;
--------------------------------------------------------------------------------
/app/main/script-executor.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process');
2 | const path = require('path');
3 | const fs = require('fs');
4 |
5 | class ScriptExecutor {
6 | constructor() {
7 | this.launchedProcesses = new Map(); // 跟踪已启动的进程
8 | }
9 |
10 | // 启动脚本(替代原来的executeScript)
11 | async launchScript(scriptId, scriptData) {
12 | try {
13 | console.log(`准备启动脚本: ${scriptData.name} (${scriptId})`);
14 |
15 | // 验证脚本文件是否存在
16 | if (!fs.existsSync(scriptData.path)) {
17 | throw new Error(`脚本文件不存在: ${scriptData.path}`);
18 | }
19 |
20 | // 根据脚本类型确定启动命令
21 | const command = this.getScriptCommand(scriptData.type, scriptData.path);
22 |
23 | console.log(`启动命令: ${command.cmd} ${command.args.join(' ')}`);
24 |
25 | // 在Windows上,使用cmd /c start来在新窗口中启动脚本
26 | let finalCmd, finalArgs;
27 |
28 | if (process.platform === 'win32') {
29 | // Windows: 在新控制台窗口中启动
30 | // 使用绝对路径避免相对路径问题
31 | const absoluteScriptPath = path.resolve(scriptData.path);
32 | const absoluteCommand = this.getScriptCommand(scriptData.type, absoluteScriptPath);
33 |
34 | finalCmd = 'cmd';
35 | finalArgs = ['/c', 'start', '/D', path.dirname(absoluteScriptPath), absoluteCommand.cmd, ...absoluteCommand.args];
36 | } else if (process.platform === 'darwin') {
37 | // macOS: 使用 Terminal.app 在新窗口中启动脚本
38 | const absoluteScriptPath = path.resolve(scriptData.path);
39 | const absoluteCommand = this.getScriptCommand(scriptData.type, absoluteScriptPath);
40 |
41 | // 构建要在终端中执行的命令
42 | const terminalCommand = `cd "${path.dirname(absoluteScriptPath)}" && ${absoluteCommand.cmd} ${absoluteCommand.args.join(' ')}`;
43 |
44 | finalCmd = 'osascript';
45 | finalArgs = ['-e', `tell application "Terminal" to do script "${terminalCommand.replace(/"/g, '\\"')}"`];
46 | } else {
47 | // Linux: 尝试使用常见的终端模拟器
48 | const absoluteScriptPath = path.resolve(scriptData.path);
49 | const absoluteCommand = this.getScriptCommand(scriptData.type, absoluteScriptPath);
50 |
51 | // 尝试使用 gnome-terminal,如果不存在则直接执行
52 | finalCmd = 'gnome-terminal';
53 | finalArgs = ['--', 'bash', '-c', `cd "${path.dirname(absoluteScriptPath)}" && ${absoluteCommand.cmd} ${absoluteCommand.args.join(' ')}; read -p "Press Enter to continue..."`];
54 | }
55 |
56 | // 确定工作目录 - 统一使用绝对路径
57 | const workingDir = path.dirname(path.resolve(scriptData.path));
58 |
59 | const childProcess = spawn(finalCmd, finalArgs, {
60 | detached: true, // 独立进程
61 | stdio: 'ignore', // 不捕获输出
62 | shell: false, // 不使用shell(因为我们已经用cmd处理了)
63 | cwd: workingDir, // 设置工作目录
64 | env: {
65 | ...process.env,
66 | PYTHONIOENCODING: 'utf-8',
67 | PYTHONUNBUFFERED: '1'
68 | }
69 | });
70 |
71 | // 分离进程,让它完全独立运行
72 | childProcess.unref();
73 |
74 | // 记录启动的进程
75 | this.launchedProcesses.set(scriptId, {
76 | pid: childProcess.pid,
77 | name: scriptData.name,
78 | startTime: new Date(),
79 | process: childProcess
80 | });
81 |
82 | // 立即返回启动成功(启动器模式)
83 | return {
84 | success: true,
85 | message: `脚本 "${scriptData.name}" 已在新窗口中启动`,
86 | pid: childProcess.pid
87 | };
88 |
89 | } catch (error) {
90 | console.error(`启动脚本异常:`, error);
91 | return {
92 | success: false,
93 | error: error.message
94 | };
95 | }
96 | }
97 |
98 | // 获取脚本启动命令
99 | getScriptCommand(type, scriptPath) {
100 | const commands = {
101 | python: {
102 | // 根据文件扩展名选择合适的Python执行器
103 | cmd: this.getPythonExecutor(scriptPath),
104 | args: ['-u', scriptPath]
105 | },
106 | javascript: {
107 | cmd: 'node',
108 | args: [scriptPath]
109 | },
110 | typescript: {
111 | cmd: 'ts-node',
112 | args: [scriptPath]
113 | },
114 | batch: {
115 | cmd: 'cmd',
116 | args: ['/c', scriptPath]
117 | },
118 | powershell: {
119 | cmd: 'powershell',
120 | args: ['-ExecutionPolicy', 'Bypass', '-File', scriptPath]
121 | },
122 | bash: {
123 | cmd: 'bash',
124 | args: [scriptPath]
125 | }
126 | };
127 |
128 | const command = commands[type];
129 | if (!command) {
130 | // 对于未知类型,尝试直接执行
131 | return {
132 | cmd: scriptPath,
133 | args: []
134 | };
135 | }
136 |
137 | return command;
138 | }
139 |
140 | // 根据Python文件扩展名选择合适的执行器
141 | getPythonExecutor(scriptPath) {
142 | const ext = path.extname(scriptPath).toLowerCase();
143 |
144 | // 在Windows上,.pyw文件使用pythonw.exe(无控制台窗口)
145 | if (process.platform === 'win32' && ext === '.pyw') {
146 | return 'pythonw';
147 | }
148 |
149 | // 其他情况使用标准python命令
150 | return 'python';
151 | }
152 |
153 | // 获取已启动的进程列表
154 | getLaunchedProcesses() {
155 | const processes = [];
156 | for (const [scriptId, processInfo] of this.launchedProcesses) {
157 | processes.push({
158 | scriptId,
159 | pid: processInfo.pid,
160 | name: processInfo.name,
161 | startTime: processInfo.startTime
162 | });
163 | }
164 | return processes;
165 | }
166 |
167 | // 清理已结束的进程记录
168 | cleanupProcesses() {
169 | for (const [scriptId, processInfo] of this.launchedProcesses) {
170 | try {
171 | // 检查进程是否还在运行
172 | process.kill(processInfo.pid, 0);
173 | } catch (error) {
174 | // 进程已结束,从记录中移除
175 | this.launchedProcesses.delete(scriptId);
176 | console.log(`清理已结束的进程记录: ${processInfo.name} (PID: ${processInfo.pid})`);
177 | }
178 | }
179 | }
180 |
181 | // 停止特定脚本(如果需要的话)
182 | async stopScript(scriptId) {
183 | const processInfo = this.launchedProcesses.get(scriptId);
184 | if (!processInfo) {
185 | return { success: false, error: '进程不存在或已结束' };
186 | }
187 |
188 | try {
189 | process.kill(processInfo.pid, 'SIGTERM');
190 | this.launchedProcesses.delete(scriptId);
191 | return {
192 | success: true,
193 | message: `已停止脚本: ${processInfo.name}`
194 | };
195 | } catch (error) {
196 | return {
197 | success: false,
198 | error: `停止脚本失败: ${error.message}`
199 | };
200 | }
201 | }
202 | }
203 |
204 | module.exports = ScriptExecutor;
--------------------------------------------------------------------------------
/app/main/script-manager.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs').promises;
2 | const path = require('path');
3 | const { app } = require('electron');
4 | const os = require('os');
5 |
6 | class ScriptManager {
7 | constructor() {
8 | // 在便携版中使用用户目录存储数据
9 | const userDataPath = app.getPath('userData') || path.join(os.homedir(), '.script-manager');
10 | this.dataFile = path.join(userDataPath, 'scripts.json');
11 | this.ensureDataFile();
12 | }
13 |
14 | async ensureDataFile() {
15 | try {
16 | // 确保数据目录存在
17 | const dataDir = path.dirname(this.dataFile);
18 | await fs.mkdir(dataDir, { recursive: true });
19 |
20 | // 检查数据文件是否存在
21 | try {
22 | await fs.access(this.dataFile);
23 | } catch (error) {
24 | // 文件不存在,创建初始数据
25 | await this.createInitialData();
26 | }
27 | } catch (error) {
28 | console.error('初始化数据文件失败:', error);
29 | }
30 | }
31 |
32 | async createInitialData() {
33 | console.log('ScriptManager: 创建初始脚本数据...');
34 |
35 | // 便携版不包含示例脚本,创建空的脚本列表
36 | const initialScripts = [];
37 |
38 | try {
39 | await fs.writeFile(this.dataFile, JSON.stringify(initialScripts, null, 2), 'utf8');
40 | console.log('ScriptManager: 已成功创建初始脚本数据,包含', initialScripts.length, '个示例脚本');
41 | } catch (error) {
42 | console.error('ScriptManager: 写入初始数据失败:', error.message);
43 | throw error;
44 | }
45 | }
46 |
47 | async loadScripts() {
48 | try {
49 | console.log('ScriptManager: 开始读取脚本数据文件:', this.dataFile);
50 | const data = await fs.readFile(this.dataFile, 'utf8');
51 | const scripts = JSON.parse(data);
52 | console.log('ScriptManager: 成功读取脚本数据,共', scripts.length, '个脚本');
53 | return { success: true, scripts };
54 | } catch (error) {
55 | console.error('ScriptManager: 读取脚本数据失败:', error.message);
56 | // 如果文件不存在,尝试创建初始数据
57 | if (error.code === 'ENOENT') {
58 | console.log('ScriptManager: 数据文件不存在,创建初始数据...');
59 | try {
60 | await this.createInitialData();
61 | const data = await fs.readFile(this.dataFile, 'utf8');
62 | const scripts = JSON.parse(data);
63 | console.log('ScriptManager: 成功创建并读取初始数据,共', scripts.length, '个脚本');
64 | return { success: true, scripts };
65 | } catch (createError) {
66 | console.error('ScriptManager: 创建初始数据失败:', createError.message);
67 | return { success: false, error: '无法创建初始数据: ' + createError.message, scripts: [] };
68 | }
69 | }
70 | return { success: false, error: error.message, scripts: [] };
71 | }
72 | }
73 |
74 | async saveScript(scriptData) {
75 | try {
76 | const { scripts } = await this.loadScripts();
77 |
78 | // 生成新的ID
79 | const newId = scripts.length > 0 ? Math.max(...scripts.map(s => s.id)) + 1 : 1;
80 |
81 | const newScript = {
82 | id: newId,
83 | name: scriptData.name,
84 | type: scriptData.type,
85 | path: scriptData.path,
86 | description: scriptData.description || '',
87 | createdAt: new Date().toISOString(),
88 | updatedAt: new Date().toISOString()
89 | };
90 |
91 | scripts.push(newScript);
92 | await fs.writeFile(this.dataFile, JSON.stringify(scripts, null, 2), 'utf8');
93 |
94 | return { success: true, script: newScript };
95 | } catch (error) {
96 | console.error('保存脚本失败:', error);
97 | return { success: false, error: error.message };
98 | }
99 | }
100 |
101 | async updateScript(scriptId, scriptData) {
102 | try {
103 | const { scripts } = await this.loadScripts();
104 | const index = scripts.findIndex(s => s.id === scriptId);
105 |
106 | if (index === -1) {
107 | return { success: false, error: '脚本不存在' };
108 | }
109 |
110 | scripts[index] = {
111 | ...scripts[index],
112 | name: scriptData.name,
113 | type: scriptData.type,
114 | path: scriptData.path,
115 | description: scriptData.description,
116 | updatedAt: new Date().toISOString()
117 | };
118 |
119 | await fs.writeFile(this.dataFile, JSON.stringify(scripts, null, 2), 'utf8');
120 |
121 | return { success: true, script: scripts[index] };
122 | } catch (error) {
123 | console.error('更新脚本失败:', error);
124 | return { success: false, error: error.message };
125 | }
126 | }
127 |
128 | async deleteScript(scriptId) {
129 | try {
130 | const { scripts } = await this.loadScripts();
131 | const filteredScripts = scripts.filter(s => s.id !== scriptId);
132 |
133 | if (filteredScripts.length === scripts.length) {
134 | return { success: false, error: '脚本不存在' };
135 | }
136 |
137 | await fs.writeFile(this.dataFile, JSON.stringify(filteredScripts, null, 2), 'utf8');
138 |
139 | return { success: true };
140 | } catch (error) {
141 | console.error('删除脚本失败:', error);
142 | return { success: false, error: error.message };
143 | }
144 | }
145 |
146 | async getScript(scriptId) {
147 | try {
148 | const { scripts } = await this.loadScripts();
149 | return scripts.find(s => s.id === scriptId);
150 | } catch (error) {
151 | console.error('获取脚本失败:', error);
152 | return null;
153 | }
154 | }
155 |
156 | async searchScripts(query) {
157 | try {
158 | const { scripts } = await this.loadScripts();
159 | const lowercaseQuery = query.toLowerCase();
160 |
161 | const filteredScripts = scripts.filter(script =>
162 | script.name.toLowerCase().includes(lowercaseQuery) ||
163 | script.type.toLowerCase().includes(lowercaseQuery) ||
164 | script.description.toLowerCase().includes(lowercaseQuery) ||
165 | script.path.toLowerCase().includes(lowercaseQuery)
166 | );
167 |
168 | return { success: true, scripts: filteredScripts };
169 | } catch (error) {
170 | console.error('搜索脚本失败:', error);
171 | return { success: false, error: error.message, scripts: [] };
172 | }
173 | }
174 |
175 | async getScriptsByType(type) {
176 | try {
177 | const { scripts } = await this.loadScripts();
178 | const filteredScripts = scripts.filter(script => script.type === type);
179 |
180 | return { success: true, scripts: filteredScripts };
181 | } catch (error) {
182 | console.error('按类型获取脚本失败:', error);
183 | return { success: false, error: error.message, scripts: [] };
184 | }
185 | }
186 | }
187 |
188 | module.exports = ScriptManager;
--------------------------------------------------------------------------------
/app/main/settings-manager.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs').promises;
2 | const path = require('path');
3 | const { app } = require('electron');
4 | const os = require('os');
5 |
6 | class SettingsManager {
7 | constructor() {
8 | // 在便携版中使用用户目录存储数据
9 | const userDataPath = app.getPath('userData') || path.join(os.homedir(), '.script-manager');
10 | this.dataFile = path.join(userDataPath, 'settings.json');
11 | this.settings = null;
12 | this.ensureDataFile();
13 | }
14 |
15 | async ensureDataFile() {
16 | try {
17 | // 确保数据目录存在
18 | const dataDir = path.dirname(this.dataFile);
19 | await fs.mkdir(dataDir, { recursive: true });
20 |
21 | // 检查数据文件是否存在
22 | try {
23 | await fs.access(this.dataFile);
24 | } catch (error) {
25 | // 文件不存在,创建初始数据
26 | await this.createInitialData();
27 | }
28 | } catch (error) {
29 | console.error('初始化设置文件失败:', error);
30 | }
31 | }
32 |
33 | async createInitialData() {
34 | console.log('SettingsManager: 创建初始设置数据...');
35 |
36 | const initialSettings = this.getDefaultSettings();
37 |
38 | try {
39 | await fs.writeFile(this.dataFile, JSON.stringify(initialSettings, null, 2), 'utf8');
40 | console.log('SettingsManager: 已成功创建初始设置数据');
41 | } catch (error) {
42 | console.error('SettingsManager: 写入初始设置失败:', error.message);
43 | throw error;
44 | }
45 | }
46 |
47 | getDefaultSettings() {
48 | return {
49 | theme: 'light',
50 | autoRefresh: true,
51 | showNotifications: true,
52 | pythonPath: '',
53 | nodePath: '',
54 | tsNodePath: '',
55 | firstRun: true,
56 | language: 'zh-CN',
57 | autoCleanup: true,
58 | maxProcesses: 10,
59 | minimizeToTray: false,
60 | updatedAt: new Date().toISOString()
61 | };
62 | }
63 |
64 | async loadSettings() {
65 | try {
66 | if (this.settings) {
67 | return { success: true, settings: this.settings };
68 | }
69 |
70 | console.log('SettingsManager: 开始读取设置数据文件:', this.dataFile);
71 | const data = await fs.readFile(this.dataFile, 'utf8');
72 | this.settings = JSON.parse(data);
73 |
74 | // 确保所有默认设置字段都存在
75 | const defaultSettings = this.getDefaultSettings();
76 | this.settings = { ...defaultSettings, ...this.settings };
77 |
78 | console.log('SettingsManager: 成功读取设置数据');
79 | return { success: true, settings: this.settings };
80 | } catch (error) {
81 | console.error('SettingsManager: 读取设置数据失败:', error.message);
82 |
83 | // 如果文件不存在,尝试创建初始数据
84 | if (error.code === 'ENOENT') {
85 | console.log('SettingsManager: 设置文件不存在,创建初始数据...');
86 | try {
87 | await this.createInitialData();
88 | const data = await fs.readFile(this.dataFile, 'utf8');
89 | this.settings = JSON.parse(data);
90 | console.log('SettingsManager: 成功创建并读取初始数据');
91 | return { success: true, settings: this.settings };
92 | } catch (createError) {
93 | console.error('SettingsManager: 创建初始数据失败:', createError.message);
94 | return { success: false, error: '无法创建初始设置数据: ' + createError.message };
95 | }
96 | }
97 | return { success: false, error: error.message };
98 | }
99 | }
100 |
101 | async saveSettings(newSettings) {
102 | try {
103 | // 加载当前设置
104 | await this.loadSettings();
105 |
106 | // 合并新设置
107 | this.settings = {
108 | ...this.settings,
109 | ...newSettings,
110 | updatedAt: new Date().toISOString()
111 | };
112 |
113 | // 写入文件
114 | await fs.writeFile(this.dataFile, JSON.stringify(this.settings, null, 2), 'utf8');
115 | console.log('SettingsManager: 设置已保存');
116 |
117 | return { success: true, settings: this.settings };
118 | } catch (error) {
119 | console.error('SettingsManager: 保存设置失败:', error.message);
120 | return { success: false, error: error.message };
121 | }
122 | }
123 |
124 | async getSetting(key) {
125 | try {
126 | const { success, settings } = await this.loadSettings();
127 | if (!success) {
128 | throw new Error('无法加载设置');
129 | }
130 |
131 | return { success: true, value: settings[key] };
132 | } catch (error) {
133 | console.error(`SettingsManager: 获取设置 ${key} 失败:`, error.message);
134 | return { success: false, error: error.message };
135 | }
136 | }
137 | }
138 |
139 | module.exports = SettingsManager;
--------------------------------------------------------------------------------
/app/main/task-scheduler.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs').promises;
2 | const path = require('path');
3 | const { app } = require('electron');
4 | const os = require('os');
5 |
6 | /**
7 | * 轻量级定时任务调度器
8 | * 专为个人脚本启动器设计,提供简单实用的定时功能
9 | */
10 | class TaskScheduler {
11 | constructor(scriptExecutor, scriptManager) {
12 | this.scriptExecutor = scriptExecutor;
13 | this.scriptManager = scriptManager;
14 | this.tasks = new Map(); // 存储任务配置
15 | this.timers = new Map(); // 存储定时器
16 |
17 | // 在便携版中使用用户目录存储数据
18 | const userDataPath = app.getPath('userData') || path.join(os.homedir(), '.script-manager');
19 | this.dataFile = path.join(userDataPath, 'tasks.json');
20 | this.isRunning = false;
21 |
22 | this.init();
23 | }
24 |
25 | async init() {
26 | await this.loadTasks();
27 | this.startScheduler();
28 | }
29 |
30 | /**
31 | * 加载任务配置
32 | */
33 | async loadTasks() {
34 | try {
35 | // 确保数据目录存在
36 | const dataDir = path.dirname(this.dataFile);
37 | await fs.mkdir(dataDir, { recursive: true });
38 |
39 | // 尝试读取任务文件
40 | try {
41 | const data = await fs.readFile(this.dataFile, 'utf8');
42 | const tasks = JSON.parse(data);
43 |
44 | tasks.forEach(task => {
45 | this.tasks.set(task.id, task);
46 | });
47 |
48 | console.log(`TaskScheduler: 加载了 ${tasks.length} 个定时任务`);
49 | } catch (error) {
50 | if (error.code === 'ENOENT') {
51 | // 文件不存在,创建空任务列表
52 | await this.saveTasks();
53 | console.log('TaskScheduler: 创建了新的任务配置文件');
54 | } else {
55 | throw error;
56 | }
57 | }
58 | } catch (error) {
59 | console.error('TaskScheduler: 加载任务失败:', error);
60 | }
61 | }
62 |
63 | /**
64 | * 保存任务配置
65 | */
66 | async saveTasks() {
67 | try {
68 | const tasks = Array.from(this.tasks.values());
69 | await fs.writeFile(this.dataFile, JSON.stringify(tasks, null, 2), 'utf8');
70 | } catch (error) {
71 | console.error('TaskScheduler: 保存任务失败:', error);
72 | throw error;
73 | }
74 | }
75 |
76 | /**
77 | * 启动调度器
78 | */
79 | startScheduler() {
80 | if (this.isRunning) return;
81 |
82 | this.isRunning = true;
83 | console.log('TaskScheduler: 调度器已启动');
84 |
85 | // 为所有启用的任务设置定时器
86 | this.tasks.forEach(task => {
87 | if (task.enabled) {
88 | this.scheduleTask(task);
89 | }
90 | });
91 | }
92 |
93 | /**
94 | * 停止调度器
95 | */
96 | stopScheduler() {
97 | this.isRunning = false;
98 |
99 | // 清除所有定时器
100 | this.timers.forEach(timer => {
101 | clearTimeout(timer);
102 | });
103 | this.timers.clear();
104 |
105 | console.log('TaskScheduler: 调度器已停止');
106 | }
107 |
108 | /**
109 | * 创建定时任务
110 | */
111 | async createTask(taskData) {
112 | try {
113 | // 如果没有提供任务名称,尝试使用脚本文件名
114 | let taskName = taskData.name;
115 | if (!taskName) {
116 | // 获取脚本数据
117 | const scriptData = await this.getScriptData(taskData.scriptId);
118 | if (scriptData && scriptData.path) {
119 | // 从路径中提取文件名作为任务名称
120 | taskName = this.getFileNameFromPath(scriptData.path);
121 | }
122 |
123 | // 如果仍然无法获取名称,使用默认命名方式
124 | if (!taskName) {
125 | taskName = `脚本${taskData.scriptId}的定时任务`;
126 | }
127 | }
128 |
129 | const task = {
130 | id: this.generateTaskId(),
131 | scriptId: taskData.scriptId,
132 | name: taskName,
133 | schedule: taskData.schedule, // { type: 'interval', value: 3600000 } 或 { type: 'daily', time: '09:00' }
134 | enabled: true,
135 | createdAt: new Date().toISOString(),
136 | lastRun: null,
137 | nextRun: this.calculateNextRun(taskData.schedule),
138 | runCount: 0
139 | };
140 |
141 | this.tasks.set(task.id, task);
142 | await this.saveTasks();
143 |
144 | if (task.enabled && this.isRunning) {
145 | this.scheduleTask(task);
146 | }
147 |
148 | return { success: true, task };
149 | } catch (error) {
150 | console.error('TaskScheduler: 创建任务失败:', error);
151 | return { success: false, error: error.message };
152 | }
153 | }
154 |
155 | /**
156 | * 更新任务
157 | */
158 | async updateTask(taskId, updates) {
159 | try {
160 | const task = this.tasks.get(taskId);
161 | if (!task) {
162 | return { success: false, error: '任务不存在' };
163 | }
164 |
165 | // 更新任务配置
166 | Object.assign(task, updates);
167 | task.updatedAt = new Date().toISOString();
168 |
169 | // 重新计算下次运行时间
170 | if (updates.schedule) {
171 | task.nextRun = this.calculateNextRun(updates.schedule);
172 | }
173 |
174 | await this.saveTasks();
175 |
176 | // 重新调度任务
177 | this.unscheduleTask(taskId);
178 | if (task.enabled && this.isRunning) {
179 | this.scheduleTask(task);
180 | }
181 |
182 | return { success: true, task };
183 | } catch (error) {
184 | console.error('TaskScheduler: 更新任务失败:', error);
185 | return { success: false, error: error.message };
186 | }
187 | }
188 |
189 | /**
190 | * 删除任务
191 | */
192 | async deleteTask(taskId) {
193 | try {
194 | this.unscheduleTask(taskId);
195 | this.tasks.delete(taskId);
196 | await this.saveTasks();
197 |
198 | return { success: true };
199 | } catch (error) {
200 | console.error('TaskScheduler: 删除任务失败:', error);
201 | return { success: false, error: error.message };
202 | }
203 | }
204 |
205 | /**
206 | * 删除指定脚本的所有相关任务
207 | */
208 | async deleteTasksByScript(scriptId) {
209 | try {
210 | const tasksToDelete = [];
211 |
212 | // 查找所有相关任务
213 | this.tasks.forEach(task => {
214 | if (task.scriptId === scriptId) {
215 | tasksToDelete.push(task);
216 | }
217 | });
218 |
219 | // 删除找到的任务
220 | let deletedCount = 0;
221 | for (const task of tasksToDelete) {
222 | this.unscheduleTask(task.id);
223 | this.tasks.delete(task.id);
224 | deletedCount++;
225 | console.log(`TaskScheduler: 删除脚本 ${scriptId} 的任务: ${task.name}`);
226 | }
227 |
228 | // 保存更改
229 | if (deletedCount > 0) {
230 | await this.saveTasks();
231 | }
232 |
233 | return {
234 | success: true,
235 | deletedCount,
236 | message: `已删除 ${deletedCount} 个相关任务`
237 | };
238 | } catch (error) {
239 | console.error('TaskScheduler: 删除脚本任务失败:', error);
240 | return { success: false, error: error.message, deletedCount: 0 };
241 | }
242 | }
243 |
244 | /**
245 | * 获取所有任务
246 | */
247 | getTasks() {
248 | return Array.from(this.tasks.values());
249 | }
250 |
251 | /**
252 | * 获取脚本的定时任务
253 | */
254 | getTasksByScript(scriptId) {
255 | return this.getTasks().filter(task => task.scriptId === scriptId);
256 | }
257 |
258 | /**
259 | * 启用/禁用任务
260 | */
261 | async toggleTask(taskId, enabled) {
262 | return await this.updateTask(taskId, { enabled });
263 | }
264 |
265 | /**
266 | * 立即执行任务
267 | */
268 | async runTaskNow(taskId) {
269 | const task = this.tasks.get(taskId);
270 | if (!task) {
271 | return { success: false, error: '任务不存在' };
272 | }
273 |
274 | return await this.executeTask(task);
275 | }
276 |
277 | /**
278 | * 调度单个任务
279 | */
280 | scheduleTask(task) {
281 | const now = new Date();
282 | const nextRun = new Date(task.nextRun);
283 | const delay = nextRun.getTime() - now.getTime();
284 |
285 | // 清除现有的定时器
286 | this.unscheduleTask(task.id);
287 |
288 | if (delay <= 0) {
289 | // 如果时间已过,重新计算下次运行时间
290 | task.nextRun = this.calculateNextRun(task.schedule);
291 | this.saveTasks(); // 保存更新的时间
292 |
293 | // 重新调度
294 | this.scheduleTask(task);
295 | return;
296 | }
297 |
298 | const timer = setTimeout(() => {
299 | this.executeTask(task);
300 | }, delay);
301 |
302 | this.timers.set(task.id, timer);
303 | console.log(`TaskScheduler: 任务 ${task.name} 已调度,将在 ${nextRun.toLocaleString()} 执行`);
304 | }
305 |
306 | /**
307 | * 取消调度任务
308 | */
309 | unscheduleTask(taskId) {
310 | const timer = this.timers.get(taskId);
311 | if (timer) {
312 | clearTimeout(timer);
313 | this.timers.delete(taskId);
314 | }
315 | }
316 |
317 | /**
318 | * 执行任务
319 | */
320 | async executeTask(task) {
321 | try {
322 | console.log(`TaskScheduler: 开始执行任务 ${task.name}`);
323 |
324 | // 获取脚本信息
325 | const scriptData = await this.getScriptData(task.scriptId);
326 | if (!scriptData) {
327 | throw new Error(`脚本 ID ${task.scriptId} 不存在`);
328 | }
329 |
330 | // 更新任务状态
331 | task.lastRun = new Date().toISOString();
332 | task.runCount++;
333 | task.nextRun = this.calculateNextRun(task.schedule);
334 |
335 | await this.saveTasks();
336 |
337 | // 执行脚本 - 使用原始脚本数据,不修改name避免Windows命令问题
338 | const result = await this.scriptExecutor.launchScript(task.scriptId, scriptData);
339 |
340 | console.log(`TaskScheduler: 任务 ${task.name} 执行完成:`, result.success ? '成功' : '失败');
341 |
342 | // 重新调度下次执行
343 | if (task.enabled && this.isRunning) {
344 | this.scheduleTask(task);
345 | }
346 |
347 | return result;
348 | } catch (error) {
349 | console.error(`TaskScheduler: 执行任务 ${task.name} 失败:`, error);
350 |
351 | // 即使执行失败也要重新调度
352 | if (task.enabled && this.isRunning) {
353 | this.scheduleTask(task);
354 | }
355 |
356 | return { success: false, error: error.message };
357 | }
358 | }
359 |
360 | /**
361 | * 获取脚本数据
362 | */
363 | async getScriptData(scriptId) {
364 | try {
365 | if (this.scriptManager && this.scriptManager.getScript) {
366 | return await this.scriptManager.getScript(scriptId);
367 | }
368 |
369 | // 如果没有scriptManager,直接从文件读取
370 | const userDataPath = app.getPath('userData') || path.join(os.homedir(), '.script-manager');
371 | const scriptsFile = path.join(userDataPath, 'scripts.json');
372 | const data = await fs.readFile(scriptsFile, 'utf8');
373 | const scripts = JSON.parse(data);
374 |
375 | return scripts.find(script => script.id === scriptId);
376 | } catch (error) {
377 | console.error('获取脚本数据失败:', error);
378 | return null;
379 | }
380 | }
381 |
382 | /**
383 | * 计算下次运行时间
384 | */
385 | calculateNextRun(schedule) {
386 | const now = new Date();
387 |
388 | switch (schedule.type) {
389 | case 'interval':
390 | // 间隔执行(毫秒)
391 | return new Date(now.getTime() + schedule.value);
392 |
393 | case 'daily':
394 | // 每日定时执行
395 | const [hours, minutes] = schedule.time.split(':').map(Number);
396 | const nextRun = new Date(now);
397 | nextRun.setHours(hours, minutes, 0, 0);
398 |
399 | // 如果今天的时间已过,设置为明天
400 | if (nextRun <= now) {
401 | nextRun.setDate(nextRun.getDate() + 1);
402 | }
403 |
404 | return nextRun;
405 |
406 | case 'weekly':
407 | // 每周定时执行
408 | const targetDay = schedule.day; // 0-6 (周日到周六)
409 | const [weekHours, weekMinutes] = schedule.time.split(':').map(Number);
410 | const weeklyNext = new Date(now);
411 |
412 | weeklyNext.setHours(weekHours, weekMinutes, 0, 0);
413 |
414 | const daysUntilTarget = (targetDay - now.getDay() + 7) % 7;
415 | if (daysUntilTarget === 0 && weeklyNext <= now) {
416 | weeklyNext.setDate(weeklyNext.getDate() + 7);
417 | } else {
418 | weeklyNext.setDate(weeklyNext.getDate() + daysUntilTarget);
419 | }
420 |
421 | return weeklyNext;
422 |
423 | default:
424 | throw new Error(`不支持的调度类型: ${schedule.type}`);
425 | }
426 | }
427 |
428 | /**
429 | * 生成任务ID
430 | */
431 | generateTaskId() {
432 | return `task_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
433 | }
434 |
435 | /**
436 | * 从文件路径中提取文件名(不包括扩展名)
437 | */
438 | getFileNameFromPath(filePath) {
439 | if (!filePath) return '';
440 |
441 | // 提取文件名(带扩展名)
442 | const fileName = path.basename(filePath);
443 |
444 | // 去除扩展名
445 | return path.parse(fileName).name;
446 | }
447 |
448 | /**
449 | * 获取调度器状态
450 | */
451 | getStatus() {
452 | return {
453 | isRunning: this.isRunning,
454 | totalTasks: this.tasks.size,
455 | activeTasks: Array.from(this.tasks.values()).filter(t => t.enabled).length,
456 | scheduledTasks: this.timers.size
457 | };
458 | }
459 | }
460 |
461 | module.exports = TaskScheduler;
462 |
--------------------------------------------------------------------------------
/app/renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 脚本管理器
7 |
8 |
9 |
10 |
11 |
12 |
48 |
49 |
50 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
🚀
153 |
欢迎使用脚本管理器
154 |
点击"添加脚本"开始管理您的脚本,或者将脚本文件拖拽到这里。
155 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
正在加载脚本...
166 |
167 |
168 |
169 |
170 |
📭
171 |
没有找到脚本
172 |
尝试调整搜索条件或添加新脚本。
173 |
174 |
175 |
176 |
177 |
178 |
211 |
212 |
213 |
214 |
215 |
216 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
--------------------------------------------------------------------------------
/app/renderer/styles.css:
--------------------------------------------------------------------------------
1 | /* 脚本管理器 - 现代化卡片布局样式 */
2 |
3 | /* 主题变量 */
4 | :root {
5 | --bg-color: #f8fafc;
6 | --header-bg: #ffffff;
7 | --card-bg: #ffffff;
8 | --text-color: #333;
9 | --text-secondary: #64748b;
10 | --border-color: #e2e8f0;
11 | --primary-color: #3b82f6;
12 | --primary-hover: #2563eb;
13 | --secondary-bg: #f1f5f9;
14 | --secondary-hover: #e2e8f0;
15 | --secondary-text: #475569;
16 | --shadow-color: rgba(0, 0, 0, 0.1);
17 | --notification-bg: #ffffff;
18 | --modal-bg: #ffffff;
19 | --input-bg: #f9fafb;
20 | --input-focus-bg: #ffffff;
21 | --danger-color: #ef4444;
22 | --danger-hover: #dc2626;
23 | --success-color: #10b981;
24 | --warning-color: #f59e0b;
25 | --info-color: #3b82f6;
26 | }
27 |
28 | /* 深色主题 */
29 | html[data-theme="dark"] {
30 | --bg-color: #0a0e17;
31 | --header-bg: #070b12;
32 | --card-bg: #111827;
33 | --text-color: #f8fafc;
34 | --text-secondary: #e2e8f0;
35 | --border-color: #334155;
36 | --primary-color: #3b82f6;
37 | --primary-hover: #60a5fa;
38 | --secondary-bg: #1e293b;
39 | --secondary-hover: #334155;
40 | --secondary-text: #e2e8f0;
41 | --shadow-color: rgba(0, 0, 0, 0.5);
42 | --notification-bg: #1e293b;
43 | --modal-bg: #0a0e17;
44 | --input-bg: #070b12;
45 | --input-focus-bg: #111827;
46 | --danger-color: #ef4444;
47 | --danger-hover: #f87171;
48 | --success-color: #10b981;
49 | --warning-color: #f59e0b;
50 | --info-color: #3b82f6;
51 | }
52 |
53 | /* 全局重置和基础样式 */
54 | * {
55 | margin: 0;
56 | padding: 0;
57 | box-sizing: border-box;
58 | }
59 |
60 | body {
61 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
62 | background: var(--bg-color);
63 | color: var(--text-color);
64 | overflow: hidden;
65 | height: 100vh;
66 | }
67 |
68 | #app {
69 | height: 100vh;
70 | display: flex;
71 | flex-direction: column;
72 | background: var(--bg-color);
73 | }
74 |
75 | /* 顶部工具栏 */
76 | .app-header {
77 | background: var(--header-bg);
78 | border-bottom: 1px solid var(--border-color);
79 | padding: 12px 24px;
80 | display: flex;
81 | align-items: center;
82 | justify-content: space-between;
83 | box-shadow: 0 1px 3px var(--shadow-color);
84 | z-index: 100;
85 | }
86 |
87 | .header-left {
88 | display: flex;
89 | align-items: center;
90 | gap: 16px;
91 | }
92 |
93 | .app-title {
94 | display: flex;
95 | align-items: center;
96 | gap: 8px;
97 | font-size: 20px;
98 | font-weight: 600;
99 | color: var(--text-color);
100 | margin: 0;
101 | }
102 |
103 | .app-icon {
104 | font-size: 24px;
105 | display: flex;
106 | align-items: center;
107 | justify-content: center;
108 | width: 24px;
109 | height: 24px;
110 | }
111 |
112 | .app-icon svg {
113 | width: 24px;
114 | height: 24px;
115 | display: block;
116 | }
117 |
118 | .version-info {
119 | background: var(--secondary-bg);
120 | color: var(--text-secondary);
121 | padding: 4px 8px;
122 | border-radius: 12px;
123 | font-size: 12px;
124 | font-weight: 500;
125 | }
126 |
127 | .header-center {
128 | flex: 1;
129 | max-width: 400px;
130 | margin: 0 32px;
131 | }
132 |
133 | .search-container {
134 | position: relative;
135 | width: 100%;
136 | }
137 |
138 | .search-input {
139 | width: 100%;
140 | padding: 10px 16px 10px 40px;
141 | border: 1px solid var(--border-color);
142 | border-radius: 20px;
143 | font-size: 14px;
144 | background: var(--input-bg);
145 | color: var(--text-color);
146 | transition: all 0.2s ease;
147 | }
148 |
149 | .search-input:focus {
150 | outline: none;
151 | border-color: var(--primary-color);
152 | background: var(--input-focus-bg);
153 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
154 | }
155 |
156 | .search-icon {
157 | position: absolute;
158 | left: 12px;
159 | top: 50%;
160 | transform: translateY(-50%);
161 | color: var(--text-secondary);
162 | font-size: 16px;
163 | }
164 |
165 | .header-right {
166 | display: flex;
167 | align-items: center;
168 | gap: 8px;
169 | }
170 |
171 | /* 按钮样式 */
172 | .btn {
173 | display: inline-flex;
174 | align-items: center;
175 | gap: 6px;
176 | padding: 8px 16px;
177 | border: none;
178 | border-radius: 8px;
179 | font-size: 14px;
180 | font-weight: 500;
181 | cursor: pointer;
182 | transition: all 0.2s ease;
183 | text-decoration: none;
184 | white-space: nowrap;
185 | }
186 |
187 | .btn-primary {
188 | background: var(--primary-color);
189 | color: white;
190 | }
191 |
192 | .btn-primary:hover {
193 | background: var(--primary-hover);
194 | transform: translateY(-1px);
195 | box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
196 | }
197 |
198 | .btn-secondary {
199 | background: var(--secondary-bg);
200 | color: var(--text-color);
201 | padding: 8px 12px;
202 | }
203 |
204 | .btn-secondary:hover {
205 | background: var(--secondary-hover);
206 | color: var(--text-color);
207 | }
208 |
209 | .btn-large {
210 | padding: 12px 24px;
211 | font-size: 16px;
212 | }
213 |
214 | .btn-icon {
215 | font-size: 16px;
216 | }
217 |
218 | /* 分类标签栏 */
219 | .category-nav {
220 | background: var(--header-bg);
221 | border-bottom: 1px solid var(--border-color);
222 | padding: 0 16px;
223 | display: flex;
224 | align-items: center;
225 | justify-content: space-between;
226 | overflow-x: auto;
227 | }
228 |
229 | .category-tabs {
230 | display: flex;
231 | gap: 2px;
232 | }
233 |
234 | .category-tab {
235 | display: flex;
236 | align-items: center;
237 | gap: 6px;
238 | padding: 10px 12px;
239 | border: none;
240 | background: none;
241 | color: var(--text-secondary);
242 | font-size: 14px;
243 | font-weight: 500;
244 | cursor: pointer;
245 | border-radius: 8px 8px 0 0;
246 | transition: all 0.2s ease;
247 | white-space: nowrap;
248 | }
249 |
250 | .category-tab:hover {
251 | background: var(--secondary-bg);
252 | color: var(--text-color);
253 | }
254 |
255 | .category-tab.active {
256 | background: var(--primary-color);
257 | color: white;
258 | }
259 |
260 | .tab-icon {
261 | font-size: 16px;
262 | }
263 |
264 | .tab-count {
265 | background: rgba(255, 255, 255, 0.2);
266 | padding: 1px 5px;
267 | border-radius: 10px;
268 | font-size: 12px;
269 | min-width: 18px;
270 | text-align: center;
271 | }
272 |
273 | .category-tab.active .tab-count {
274 | background: rgba(255, 255, 255, 0.3);
275 | }
276 |
277 | .category-actions {
278 | display: flex;
279 | align-items: center;
280 | gap: 12px;
281 | }
282 |
283 | .sort-select {
284 | padding: 6px 12px;
285 | border: 1px solid var(--border-color);
286 | border-radius: 6px;
287 | font-size: 13px;
288 | background: var(--input-bg);
289 | color: var(--text-color);
290 | cursor: pointer;
291 | }
292 |
293 | /* 主内容区域 */
294 | .main-content {
295 | flex: 1;
296 | padding: 24px;
297 | overflow-y: auto;
298 | background: var(--bg-color);
299 | }
300 |
301 | .scripts-grid {
302 | display: grid;
303 | grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
304 | gap: 20px;
305 | max-width: 100%;
306 | width: 100%;
307 | margin: 0 auto;
308 | padding: 0 20px;
309 | }
310 |
311 | /* 脚本卡片 */
312 | .script-card {
313 | background: var(--card-bg);
314 | border-radius: 12px;
315 | padding: 20px;
316 | box-shadow: 0 1px 3px var(--shadow-color);
317 | transition: all 0.2s ease;
318 | cursor: pointer;
319 | position: relative;
320 | border: 2px solid transparent;
321 | display: flex;
322 | flex-direction: column;
323 | height: 220px; /* 固定卡片高度 */
324 | }
325 |
326 | /* 新添加的脚本卡片特效 */
327 | .script-card.new-card {
328 | animation: highlight-new-card 2s ease-in-out;
329 | border-color: var(--primary-color);
330 | box-shadow: 0 0 15px var(--primary-color);
331 | }
332 |
333 | @keyframes highlight-new-card {
334 | 0% {
335 | transform: scale(1);
336 | box-shadow: 0 0 5px var(--primary-color);
337 | }
338 | 10% {
339 | transform: scale(1.03);
340 | box-shadow: 0 0 20px var(--primary-color);
341 | }
342 | 20% {
343 | transform: scale(1);
344 | box-shadow: 0 0 15px var(--primary-color);
345 | }
346 | 30% {
347 | transform: scale(1.02);
348 | box-shadow: 0 0 18px var(--primary-color);
349 | }
350 | 40% {
351 | transform: scale(1);
352 | box-shadow: 0 0 15px var(--primary-color);
353 | }
354 | 100% {
355 | transform: scale(1);
356 | box-shadow: 0 0 5px var(--primary-color);
357 | }
358 | }
359 |
360 | .script-card:hover {
361 | transform: translateY(-2px);
362 | box-shadow: 0 8px 25px var(--shadow-color);
363 | border-color: var(--primary-color);
364 | }
365 |
366 | .script-card.launching {
367 | border-color: var(--warning-color);
368 | background: var(--card-bg);
369 | }
370 |
371 | .script-card.running {
372 | border-color: var(--success-color);
373 | background: var(--card-bg);
374 | }
375 |
376 | .script-card.error {
377 | border-color: var(--danger-color);
378 | background: var(--card-bg);
379 | }
380 |
381 | .card-icon {
382 | font-size: 32px;
383 | margin-bottom: 16px; /* 增加图标与标题之间的间距 */
384 | display: block;
385 | height: 32px;
386 | }
387 |
388 | .card-title {
389 | font-size: 16px;
390 | font-weight: 600;
391 | color: var(--text-color);
392 | margin-bottom: 16px;
393 | line-height: 1.3;
394 | overflow: hidden;
395 | text-overflow: ellipsis;
396 | white-space: nowrap;
397 | height: 21px; /* 基于字体大小和行高: 16px * 1.3 = ~21px */
398 | }
399 |
400 | .card-type {
401 | position: absolute;
402 | top: 12px;
403 | right: 12px;
404 | background: var(--secondary-bg);
405 | color: var(--text-secondary);
406 | padding: 4px 8px;
407 | border-radius: 12px;
408 | font-size: 12px;
409 | font-weight: 500;
410 | height: 22px; /* 固定高度 */
411 | line-height: 14px;
412 | width: fit-content; /* 宽度适应内容 */
413 | text-align: center; /* 文本居中 */
414 | overflow: hidden; /* 处理文本溢出 */
415 | text-overflow: ellipsis; /* 文本溢出显示省略号 */
416 | white-space: nowrap; /* 防止文本换行 */
417 | max-width: 40%; /* 限制最大宽度,防止挤压其他内容 */
418 | z-index: 2; /* 确保在其他元素之上 */
419 | }
420 |
421 | .card-description {
422 | color: var(--text-secondary);
423 | font-size: 13px;
424 | line-height: 1.4;
425 | margin-top: 8px;
426 | margin-bottom: 16px;
427 | overflow: hidden;
428 | text-overflow: ellipsis;
429 | display: -webkit-box;
430 | -webkit-line-clamp: 2;
431 | line-clamp: 2;
432 | -webkit-box-orient: vertical;
433 | box-orient: vertical;
434 | height: 36px;
435 | min-height: 36px;
436 | }
437 |
438 | .card-footer {
439 | display: flex;
440 | align-items: center;
441 | justify-content: space-between;
442 | margin-top: auto;
443 | padding-top: 8px;
444 | border-top: 1px solid var(--border-color);
445 | flex-wrap: wrap;
446 | gap: 8px;
447 | }
448 |
449 | .card-status {
450 | font-size: 12px;
451 | color: var(--text-secondary);
452 | margin-right: auto;
453 | }
454 |
455 | .card-actions {
456 | display: flex;
457 | gap: 4px;
458 | opacity: 0;
459 | transition: opacity 0.2s ease;
460 | }
461 |
462 | .script-card:hover .card-actions {
463 | opacity: 1;
464 | }
465 |
466 | .card-action-btn {
467 | padding: 4px 8px;
468 | border: none;
469 | background: var(--secondary-bg);
470 | color: var(--text-secondary);
471 | border-radius: 4px;
472 | font-size: 12px;
473 | cursor: pointer;
474 | transition: all 0.2s ease;
475 | }
476 |
477 | .card-action-btn:hover {
478 | background: var(--secondary-hover);
479 | color: var(--text-color);
480 | }
481 |
482 | /* 特殊状态屏幕 */
483 | .welcome-screen,
484 | .loading-screen,
485 | .empty-screen {
486 | grid-column: 1 / -1;
487 | display: flex;
488 | flex-direction: column;
489 | align-items: center;
490 | justify-content: center;
491 | padding: 60px 20px;
492 | text-align: center;
493 | }
494 |
495 | .welcome-content {
496 | max-width: 400px;
497 | }
498 |
499 | .welcome-icon,
500 | .empty-icon {
501 | font-size: 64px;
502 | margin-bottom: 24px;
503 | opacity: 0.8;
504 | }
505 |
506 | .welcome-screen h2,
507 | .empty-screen h3 {
508 | font-size: 24px;
509 | font-weight: 600;
510 | color: var(--text-color);
511 | margin-bottom: 12px;
512 | }
513 |
514 | .welcome-screen p,
515 | .empty-screen p {
516 | color: var(--text-secondary);
517 | font-size: 16px;
518 | line-height: 1.5;
519 | margin-bottom: 24px;
520 | }
521 |
522 | .loading-spinner {
523 | width: 40px;
524 | height: 40px;
525 | border: 4px solid var(--border-color);
526 | border-top: 4px solid var(--primary-color);
527 | border-radius: 50%;
528 | animation: spin 1s linear infinite;
529 | margin-bottom: 16px;
530 | }
531 |
532 | @keyframes spin {
533 | 0% { transform: rotate(0deg); }
534 | 100% { transform: rotate(360deg); }
535 | }
536 |
537 | /* 状态栏 */
538 | .status-bar {
539 | background: var(--header-bg);
540 | border-top: 1px solid var(--border-color);
541 | padding: 8px 24px;
542 | display: flex;
543 | align-items: center;
544 | justify-content: space-between;
545 | font-size: 13px;
546 | color: var(--text-secondary);
547 | }
548 |
549 | .status-left,
550 | .status-center,
551 | .status-right {
552 | display: flex;
553 | align-items: center;
554 | gap: 16px;
555 | }
556 |
557 | .status-center {
558 | flex: 1;
559 | justify-content: center;
560 | }
561 |
562 | .status-item {
563 | display: flex;
564 | align-items: center;
565 | gap: 4px;
566 | }
567 |
568 | .status-icon {
569 | font-size: 14px;
570 | }
571 |
572 | /* GitHub链接和作者邮箱样式 */
573 | .author-email {
574 | color: var(--text-secondary);
575 | text-decoration: none;
576 | font-size: 13px;
577 | transition: color 0.2s ease;
578 | }
579 |
580 | .author-email:hover {
581 | color: var(--primary-color);
582 | text-decoration: underline;
583 | }
584 |
585 | .github-link {
586 | display: flex;
587 | align-items: center;
588 | gap: 4px;
589 | color: var(--text-secondary);
590 | text-decoration: none;
591 | font-size: 13px;
592 | transition: color 0.2s ease;
593 | cursor: pointer;
594 | }
595 |
596 | .github-link:hover {
597 | color: var(--text-color);
598 | }
599 |
600 | .github-icon {
601 | width: 16px;
602 | height: 16px;
603 | fill: currentColor;
604 | }
605 |
606 | /* 模态对话框 */
607 | .modal-overlay {
608 | position: fixed;
609 | top: 0;
610 | left: 0;
611 | right: 0;
612 | bottom: 0;
613 | background: rgba(0, 0, 0, 0.7);
614 | display: flex;
615 | align-items: center;
616 | justify-content: center;
617 | z-index: 1000;
618 | opacity: 0;
619 | visibility: hidden;
620 | transition: all 0.3s ease;
621 | }
622 |
623 | .modal-overlay[style*="display: flex"] {
624 | opacity: 1;
625 | visibility: visible;
626 | }
627 |
628 | .modal {
629 | background: var(--modal-bg);
630 | border-radius: 12px;
631 | width: 90%;
632 | max-width: 500px;
633 | max-height: 90vh;
634 | overflow-y: auto;
635 | box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
636 | transform: translateY(-20px);
637 | transition: all 0.3s ease;
638 | }
639 |
640 | .modal-overlay[style*="display: flex"] .modal {
641 | transform: translateY(0);
642 | }
643 |
644 | .modal-header {
645 | display: flex;
646 | align-items: center;
647 | justify-content: space-between;
648 | padding: 16px 24px;
649 | border-bottom: 1px solid var(--border-color);
650 | }
651 |
652 | .modal-header h3 {
653 | margin: 0;
654 | font-size: 20px;
655 | font-weight: 600;
656 | color: var(--text-color);
657 | }
658 |
659 | .modal-close {
660 | background: none;
661 | border: none;
662 | font-size: 24px;
663 | line-height: 1;
664 | color: var(--text-secondary);
665 | cursor: pointer;
666 | padding: 8px;
667 | border-radius: 50%;
668 | display: flex;
669 | align-items: center;
670 | justify-content: center;
671 | transition: all 0.2s ease;
672 | min-width: 40px;
673 | min-height: 40px;
674 | }
675 |
676 | .modal-close:hover {
677 | background: var(--secondary-bg);
678 | color: var(--text-color);
679 | }
680 |
681 | .modal-body {
682 | padding: 24px;
683 | max-height: 70vh;
684 | overflow-y: auto;
685 | }
686 |
687 | /* 右键菜单 */
688 | .context-menu {
689 | position: fixed;
690 | background: var(--modal-bg);
691 | border: 1px solid var(--border-color);
692 | border-radius: 8px;
693 | box-shadow: 0 10px 15px -3px var(--shadow-color);
694 | z-index: 1000;
695 | min-width: 160px;
696 | overflow: hidden;
697 | }
698 |
699 | .context-menu-item {
700 | display: flex;
701 | align-items: center;
702 | gap: 8px;
703 | padding: 10px 16px;
704 | font-size: 14px;
705 | color: var(--text-color);
706 | cursor: pointer;
707 | transition: background-color 0.2s ease;
708 | }
709 |
710 | .context-menu-item:hover {
711 | background: var(--secondary-bg);
712 | }
713 |
714 | .context-menu-item.danger {
715 | color: var(--danger-color);
716 | }
717 |
718 | .context-menu-item.danger:hover {
719 | background: var(--secondary-bg);
720 | }
721 |
722 | .context-menu-divider {
723 | height: 1px;
724 | background: var(--border-color);
725 | margin: 4px 0;
726 | }
727 |
728 | .menu-icon {
729 | font-size: 16px;
730 | width: 16px;
731 | text-align: center;
732 | }
733 |
734 | /* 通知系统 */
735 | .notification-container {
736 | position: fixed;
737 | bottom: 20px;
738 | right: 20px;
739 | z-index: 1100;
740 | display: flex;
741 | flex-direction: column;
742 | gap: 8px;
743 | }
744 |
745 | .notification {
746 | background: var(--notification-bg);
747 | border: 1px solid var(--border-color);
748 | border-radius: 8px;
749 | padding: 12px 16px;
750 | box-shadow: 0 4px 6px -1px var(--shadow-color);
751 | max-width: 300px;
752 | display: flex;
753 | align-items: center;
754 | gap: 8px;
755 | animation: slideIn 0.3s ease;
756 | color: var(--text-color);
757 | }
758 |
759 | .notification.success {
760 | border-left: 4px solid var(--success-color);
761 | }
762 |
763 | .notification.error {
764 | border-left: 4px solid var(--danger-color);
765 | }
766 |
767 | .notification.warning {
768 | border-left: 4px solid var(--warning-color);
769 | }
770 |
771 | .notification.info {
772 | border-left: 4px solid var(--info-color);
773 | }
774 |
775 | .notification-icon {
776 | font-size: 16px;
777 | }
778 |
779 | .notification-message {
780 | font-size: 14px;
781 | color: var(--text-color);
782 | flex: 1;
783 | }
784 |
785 | @keyframes slideIn {
786 | from {
787 | transform: translateY(100%);
788 | opacity: 0;
789 | }
790 | to {
791 | transform: translateY(0);
792 | opacity: 1;
793 | }
794 | }
795 |
796 | /* 表单样式 */
797 | .form-group {
798 | margin-bottom: 20px;
799 | }
800 |
801 | .form-label {
802 | display: block;
803 | font-size: 14px;
804 | font-weight: 500;
805 | color: var(--text-color);
806 | margin-bottom: 6px;
807 | }
808 |
809 | .form-input,
810 | .form-textarea,
811 | .form-select {
812 | width: 100%;
813 | padding: 10px 12px;
814 | border: 1px solid var(--border-color);
815 | border-radius: 6px;
816 | font-size: 14px;
817 | background: var(--input-bg);
818 | color: var(--text-color);
819 | transition: border-color 0.2s ease;
820 | }
821 |
822 | .form-input:focus,
823 | .form-textarea:focus,
824 | .form-select:focus {
825 | outline: none;
826 | border-color: var(--primary-color);
827 | background: var(--input-focus-bg);
828 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
829 | }
830 |
831 | .form-textarea {
832 | resize: vertical;
833 | min-height: 80px;
834 | }
835 |
836 | .form-actions {
837 | display: flex;
838 | gap: 12px;
839 | justify-content: flex-end;
840 | margin-top: 24px;
841 | padding-top: 20px;
842 | border-top: 1px solid var(--border-color);
843 | }
844 |
845 | /* 响应式设计 */
846 | @media (max-width: 768px) {
847 | .app-header {
848 | padding: 10px 16px;
849 | flex-wrap: wrap;
850 | }
851 |
852 | .header-center {
853 | order: 3;
854 | width: 100%;
855 | max-width: 100%;
856 | margin: 10px 0 0;
857 | }
858 |
859 | .category-nav {
860 | padding: 0 8px;
861 | overflow-x: auto;
862 | }
863 |
864 | .main-content {
865 | padding: 16px;
866 | }
867 |
868 | .scripts-grid {
869 | padding: 0;
870 | gap: 16px;
871 | grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
872 | }
873 |
874 | .script-card {
875 | padding: 16px;
876 | }
877 |
878 | .status-bar {
879 | padding: 8px 16px;
880 | flex-direction: column;
881 | align-items: flex-start;
882 | gap: 8px;
883 | }
884 |
885 | .status-center {
886 | justify-content: flex-start;
887 | }
888 |
889 | .modal {
890 | width: 95%;
891 | max-width: 450px;
892 | }
893 | }
894 |
895 | @media (max-width: 480px) {
896 | .scripts-grid {
897 | grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
898 | gap: 12px;
899 | }
900 |
901 | .category-tabs {
902 | gap: 1px;
903 | }
904 |
905 | .category-tab {
906 | padding: 8px 10px;
907 | gap: 4px;
908 | font-size: 13px;
909 | }
910 | }
911 |
912 | /* 定时任务相关样式 */
913 | .card-timer-info {
914 | display: flex;
915 | align-items: center;
916 | gap: 6px;
917 | padding: 4px 8px;
918 | background: rgba(135, 206, 250, 0.25); /* 更接近天蓝色的背景 */
919 | border-radius: 6px;
920 | font-size: 12px;
921 | color: var(--text-color);
922 | height: 24px;
923 | min-height: 24px;
924 | margin: 0;
925 | order: 1;
926 | }
927 |
928 | .timer-icon {
929 | font-size: 14px;
930 | }
931 |
932 | .timer-text {
933 | font-weight: 500;
934 | }
935 |
936 | /* 确保复选框和单选框在深色模式下可见 */
937 | input[type="checkbox"],
938 | input[type="radio"] {
939 | accent-color: var(--primary-color);
940 | width: 16px;
941 | height: 16px;
942 | margin-right: 8px;
943 | vertical-align: middle;
944 | }
945 |
946 | /* 添加复选框标签样式 */
947 | .checkbox-label {
948 | display: flex;
949 | align-items: center;
950 | cursor: pointer;
951 | color: var(--text-color);
952 | }
953 |
954 | /* 按钮变体样式 */
955 | .btn-sm {
956 | padding: 6px 12px;
957 | font-size: 13px;
958 | }
959 |
960 | .btn-danger {
961 | background: var(--danger-color);
962 | color: white;
963 | border: 1px solid var(--danger-color);
964 | }
965 |
966 | .btn-danger:hover {
967 | background: var(--danger-hover);
968 | border-color: var(--danger-hover);
969 | }
970 |
971 | /* SVG图标样式 */
972 | .icon-svg {
973 | width: 32px;
974 | height: 32px;
975 | display: inline-block;
976 | fill: currentColor;
977 | vertical-align: middle;
978 | }
979 |
980 | .tab-icon .icon-svg {
981 | width: 16px;
982 | height: 16px;
983 | }
984 |
985 | .card-icon .icon-svg {
986 | width: 32px;
987 | height: 32px;
988 | margin-bottom: 8px;
989 | }
990 |
991 | /* 主题图标样式 */
992 | .theme-icon {
993 | display: inline-block;
994 | width: 24px;
995 | height: 24px;
996 | vertical-align: middle;
997 | margin-right: 8px;
998 | }
999 |
1000 | /* 确认对话框样式 */
1001 | .confirm-dialog {
1002 | display: flex;
1003 | flex-direction: column;
1004 | gap: 24px;
1005 | padding: 8px;
1006 | }
1007 |
1008 | .confirm-message {
1009 | font-size: 15px;
1010 | line-height: 1.6;
1011 | color: var(--text-color);
1012 | white-space: pre-line; /* 保留换行符 */
1013 | text-align: center;
1014 | }
1015 |
1016 | @media (max-width: 480px) {
1017 | .confirm-message {
1018 | font-size: 14px;
1019 | line-height: 1.5;
1020 | }
1021 |
1022 | .form-actions {
1023 | flex-direction: column-reverse;
1024 | gap: 10px;
1025 | }
1026 |
1027 | .form-actions button {
1028 | width: 100%;
1029 | }
1030 | }
--------------------------------------------------------------------------------
/app/renderer/task-manager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 定时任务管理器 - 轻量级个人使用版本
3 | * 提供简单实用的定时任务功能
4 | */
5 | class TaskManager {
6 | constructor() {
7 | this.tasks = new Map();
8 | this.currentScript = null;
9 | this.init();
10 | }
11 |
12 | init() {
13 | this.loadTasks();
14 | this.bindEvents();
15 | }
16 |
17 | /**
18 | * 加载任务列表
19 | */
20 | async loadTasks() {
21 | try {
22 | const result = await window.electronAPI.getTasks();
23 | if (result.success) {
24 | this.tasks.clear();
25 | result.tasks.forEach(task => {
26 | this.tasks.set(task.id, task);
27 | });
28 | }
29 | } catch (error) {
30 | console.error('加载任务失败:', error);
31 | }
32 | }
33 |
34 | /**
35 | * 绑定事件
36 | */
37 | bindEvents() {
38 | // 定时任务按钮点击
39 | document.getElementById('timer-btn')?.addEventListener('click', () => {
40 | this.showTaskManager();
41 | });
42 | }
43 |
44 | /**
45 | * 显示任务管理界面
46 | */
47 | showTaskManager() {
48 | const modalBody = document.getElementById('modal-body');
49 | const modalTitle = document.getElementById('modal-title');
50 |
51 | modalTitle.textContent = '定时任务管理';
52 | modalBody.innerHTML = this.renderTaskManagerHTML();
53 |
54 | // 显示模态框
55 | document.getElementById('modal-overlay').style.display = 'flex';
56 |
57 | // 绑定任务管理事件
58 | this.bindTaskManagerEvents();
59 |
60 | // 刷新任务列表
61 | this.refreshTaskList();
62 | }
63 |
64 | /**
65 | * 渲染任务管理界面HTML
66 | */
67 | renderTaskManagerHTML() {
68 | return `
69 |
91 |
92 |
204 | `;
205 | }
206 |
207 | /**
208 | * 绑定任务管理事件
209 | */
210 | bindTaskManagerEvents() {
211 | // 新建任务按钮
212 | const addTaskBtn = document.getElementById('add-task-btn');
213 | if (addTaskBtn) {
214 | console.log('绑定新建任务按钮事件');
215 | addTaskBtn.addEventListener('click', (e) => {
216 | e.preventDefault();
217 | e.stopPropagation();
218 | console.log('新建任务按钮被点击');
219 | this.showCreateTaskDialog();
220 | });
221 | } else {
222 | console.error('找不到新建任务按钮');
223 | }
224 | }
225 |
226 | /**
227 | * 刷新任务列表
228 | */
229 | async refreshTaskList() {
230 | await this.loadTasks();
231 |
232 | const taskListEl = document.getElementById('task-list');
233 | const totalTasksEl = document.getElementById('total-tasks');
234 | const activeTasksEl = document.getElementById('active-tasks');
235 |
236 | if (!taskListEl) return;
237 |
238 | const tasks = Array.from(this.tasks.values());
239 | const activeTasks = tasks.filter(t => t.enabled);
240 |
241 | // 更新统计
242 | if (totalTasksEl) totalTasksEl.textContent = tasks.length;
243 | if (activeTasksEl) activeTasksEl.textContent = activeTasks.length;
244 |
245 | // 渲染任务列表
246 | if (tasks.length === 0) {
247 | taskListEl.innerHTML = `
248 |
249 |
还没有定时任务
250 |
点击"新建任务"开始创建
251 |
252 | `;
253 | } else {
254 | taskListEl.innerHTML = tasks.map(task => this.renderTaskItem(task)).join('');
255 | this.bindTaskItemEvents();
256 | }
257 | }
258 |
259 | /**
260 | * 渲染单个任务项
261 | */
262 | renderTaskItem(task) {
263 | const scheduleText = this.formatSchedule(task.schedule);
264 | const nextRunText = task.nextRun ?
265 | `下次运行: ${new Date(task.nextRun).toLocaleString()}` :
266 | '未调度';
267 |
268 | return `
269 |
270 |
271 |
${task.name}
272 |
${scheduleText}
273 |
${nextRunText}
274 |
275 |
276 |
280 |
284 |
288 |
292 |
293 |
294 | `;
295 | }
296 |
297 | /**
298 | * 绑定任务项事件
299 | */
300 | bindTaskItemEvents() {
301 | document.querySelectorAll('[data-action]').forEach(btn => {
302 | btn.addEventListener('click', async (e) => {
303 | const taskId = e.target.dataset.taskId;
304 | const action = e.target.dataset.action;
305 |
306 | switch (action) {
307 | case 'toggle':
308 | await this.toggleTask(taskId);
309 | break;
310 | case 'run':
311 | await this.runTaskNow(taskId);
312 | break;
313 | case 'edit':
314 | await this.editTask(taskId);
315 | break;
316 | case 'delete':
317 | await this.deleteTask(taskId);
318 | break;
319 | }
320 | });
321 | });
322 | }
323 |
324 | /**
325 | * 格式化调度信息
326 | */
327 | formatSchedule(schedule) {
328 | switch (schedule.type) {
329 | case 'interval':
330 | const minutes = Math.floor(schedule.value / 60000);
331 | const hours = Math.floor(minutes / 60);
332 | if (hours > 0) {
333 | return `每 ${hours} 小时执行`;
334 | } else {
335 | return `每 ${minutes} 分钟执行`;
336 | }
337 | case 'daily':
338 | return `每天 ${schedule.time} 执行`;
339 | case 'weekly':
340 | const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
341 | return `每${days[schedule.day]} ${schedule.time} 执行`;
342 | default:
343 | return '未知调度';
344 | }
345 | }
346 |
347 | /**
348 | * 显示创建任务对话框
349 | */
350 | showCreateTaskDialog() {
351 | console.log('显示创建任务对话框');
352 |
353 | // 获取可用的脚本列表
354 | this.showCreateTaskForm();
355 | }
356 |
357 | /**
358 | * 显示创建任务表单
359 | */
360 | async showCreateTaskForm() {
361 | try {
362 | // 获取脚本列表
363 | const scriptsResult = await window.electronAPI.loadScripts();
364 | if (!scriptsResult.success) {
365 | this.showNotification('无法获取脚本列表', 'error');
366 | return;
367 | }
368 |
369 | const scripts = scriptsResult.scripts || [];
370 | if (scripts.length === 0) {
371 | this.showNotification('请先添加脚本再创建定时任务', 'warning');
372 | return;
373 | }
374 |
375 | // 更新模态框内容为创建任务表单
376 | const modalTitle = document.getElementById('modal-title');
377 | const modalBody = document.getElementById('modal-body');
378 |
379 | modalTitle.textContent = '创建定时任务';
380 | modalBody.innerHTML = this.renderCreateTaskForm(scripts);
381 |
382 | // 绑定表单事件
383 | this.bindCreateTaskFormEvents();
384 |
385 | } catch (error) {
386 | console.error('显示创建任务表单失败:', error);
387 | this.showNotification('显示创建任务表单失败: ' + error.message, 'error');
388 | }
389 | }
390 |
391 | /**
392 | * 渲染创建任务表单
393 | */
394 | renderCreateTaskForm(scripts) {
395 | return `
396 |
458 | `;
459 | }
460 |
461 | /**
462 | * 绑定创建任务表单事件
463 | */
464 | bindCreateTaskFormEvents() {
465 | const form = document.getElementById('create-task-form');
466 | const scheduleTypeSelect = document.getElementById('schedule-type');
467 | const cancelBtn = document.getElementById('cancel-create-task');
468 |
469 | // 调度类型变化时显示对应配置
470 | scheduleTypeSelect?.addEventListener('change', (e) => {
471 | const type = e.target.value;
472 |
473 | // 隐藏所有配置
474 | document.getElementById('interval-config').style.display = 'none';
475 | document.getElementById('daily-config').style.display = 'none';
476 | document.getElementById('weekly-config').style.display = 'none';
477 |
478 | // 显示对应配置
479 | if (type) {
480 | document.getElementById(`${type}-config`).style.display = 'block';
481 | }
482 | });
483 |
484 | // 取消按钮
485 | cancelBtn?.addEventListener('click', () => {
486 | this.showTaskManager(); // 返回任务管理界面
487 | });
488 |
489 | // 表单提交
490 | form?.addEventListener('submit', async (e) => {
491 | e.preventDefault();
492 | await this.handleCreateTaskSubmit();
493 | });
494 | }
495 |
496 | /**
497 | * 处理创建任务表单提交
498 | */
499 | async handleCreateTaskSubmit() {
500 | try {
501 | const scriptId = parseInt(document.getElementById('task-script-id').value);
502 | const taskName = document.getElementById('task-name').value.trim();
503 | const scheduleType = document.getElementById('schedule-type').value;
504 |
505 | if (!scriptId || !scheduleType) {
506 | this.showNotification('请填写所有必填字段', 'warning');
507 | return;
508 | }
509 |
510 | let schedule;
511 | switch (scheduleType) {
512 | case 'interval':
513 | const intervalValue = parseInt(document.getElementById('interval-value').value);
514 | const intervalUnit = document.getElementById('interval-unit').value;
515 | const milliseconds = intervalUnit === 'hours' ? intervalValue * 3600000 : intervalValue * 60000;
516 | schedule = { type: 'interval', value: milliseconds };
517 | break;
518 |
519 | case 'daily':
520 | const dailyTime = document.getElementById('daily-time').value;
521 | schedule = { type: 'daily', time: dailyTime };
522 | break;
523 |
524 | case 'weekly':
525 | const weeklyDay = parseInt(document.getElementById('weekly-day').value);
526 | const weeklyTime = document.getElementById('weekly-time').value;
527 | schedule = { type: 'weekly', day: weeklyDay, time: weeklyTime };
528 | break;
529 |
530 | default:
531 | this.showNotification('不支持的调度类型', 'error');
532 | return;
533 | }
534 |
535 | const taskData = {
536 | scriptId,
537 | schedule
538 | };
539 |
540 | if (taskName) {
541 | taskData.name = taskName;
542 | }
543 |
544 | await this.createTask(taskData);
545 |
546 | } catch (error) {
547 | console.error('创建任务失败:', error);
548 | this.showNotification('创建任务失败: ' + error.message, 'error');
549 | }
550 | }
551 |
552 | /**
553 | * 创建任务
554 | */
555 | async createTask(taskData) {
556 | try {
557 | const result = await window.electronAPI.createTask(taskData);
558 | if (result.success) {
559 | this.showNotification('任务创建成功', 'success');
560 | // 返回任务管理界面并刷新列表
561 | this.showTaskManager();
562 |
563 | // 刷新脚本管理器的脚本卡片,显示最新的定时任务状态
564 | if (window.scriptManager) {
565 | window.scriptManager.refreshScripts();
566 | }
567 | } else {
568 | this.showNotification('创建任务失败: ' + result.error, 'error');
569 | }
570 | } catch (error) {
571 | this.showNotification('创建任务失败: ' + error.message, 'error');
572 | }
573 | }
574 |
575 | /**
576 | * 切换任务状态
577 | */
578 | async toggleTask(taskId) {
579 | try {
580 | const task = this.tasks.get(taskId);
581 | if (!task) return;
582 |
583 | const result = await window.electronAPI.toggleTask(taskId, !task.enabled);
584 | if (result.success) {
585 | this.showNotification(`任务已${task.enabled ? '禁用' : '启用'}`, 'success');
586 | this.refreshTaskList();
587 |
588 | // 刷新脚本管理器的脚本卡片,显示最新的定时任务状态
589 | if (window.scriptManager) {
590 | window.scriptManager.refreshScripts();
591 | }
592 | } else {
593 | this.showNotification('操作失败: ' + result.error, 'error');
594 | }
595 | } catch (error) {
596 | this.showNotification('操作失败: ' + error.message, 'error');
597 | }
598 | }
599 |
600 | /**
601 | * 立即执行任务
602 | */
603 | async runTaskNow(taskId) {
604 | try {
605 | const result = await window.electronAPI.runTaskNow(taskId);
606 | if (result.success) {
607 | this.showNotification('任务已启动执行', 'success');
608 | } else {
609 | this.showNotification('执行失败: ' + result.error, 'error');
610 | }
611 | } catch (error) {
612 | this.showNotification('执行失败: ' + error.message, 'error');
613 | }
614 | }
615 |
616 | /**
617 | * 编辑任务
618 | */
619 | async editTask(taskId) {
620 | try {
621 | const task = this.tasks.get(taskId);
622 | if (!task) {
623 | this.showNotification('任务不存在', 'error');
624 | return;
625 | }
626 |
627 | // 获取脚本列表
628 | const scriptsResult = await window.electronAPI.loadScripts();
629 | if (!scriptsResult.success) {
630 | this.showNotification('无法获取脚本列表', 'error');
631 | return;
632 | }
633 |
634 | const scripts = scriptsResult.scripts || [];
635 |
636 | // 更新模态框内容为编辑任务表单
637 | const modalTitle = document.getElementById('modal-title');
638 | const modalBody = document.getElementById('modal-body');
639 |
640 | modalTitle.textContent = '编辑定时任务';
641 | modalBody.innerHTML = this.renderEditTaskForm(task, scripts);
642 |
643 | // 绑定编辑表单事件
644 | this.bindEditTaskFormEvents(taskId);
645 |
646 | } catch (error) {
647 | console.error('编辑任务失败:', error);
648 | this.showNotification('编辑任务失败: ' + error.message, 'error');
649 | }
650 | }
651 |
652 | /**
653 | * 渲染编辑任务表单
654 | */
655 | renderEditTaskForm(task, scripts) {
656 | return `
657 |
719 | `;
720 | }
721 |
722 | /**
723 | * 获取间隔值(用于编辑表单)
724 | */
725 | getIntervalValue(schedule) {
726 | if (schedule.type !== 'interval') return 60;
727 | const minutes = Math.floor(schedule.value / 60000);
728 | const hours = Math.floor(minutes / 60);
729 | return hours > 0 ? hours : minutes;
730 | }
731 |
732 | /**
733 | * 获取间隔单位(用于编辑表单)
734 | */
735 | getIntervalUnit(schedule) {
736 | if (schedule.type !== 'interval') return 'minutes';
737 | const minutes = Math.floor(schedule.value / 60000);
738 | return minutes >= 60 ? 'hours' : 'minutes';
739 | }
740 |
741 | /**
742 | * 绑定编辑任务表单事件
743 | */
744 | bindEditTaskFormEvents(taskId) {
745 | const form = document.getElementById('edit-task-form');
746 | const scheduleTypeSelect = document.getElementById('edit-schedule-type');
747 | const cancelBtn = document.getElementById('cancel-edit-task');
748 |
749 | // 调度类型变化时显示对应配置
750 | scheduleTypeSelect?.addEventListener('change', (e) => {
751 | const type = e.target.value;
752 |
753 | // 隐藏所有配置
754 | document.getElementById('edit-interval-config').style.display = 'none';
755 | document.getElementById('edit-daily-config').style.display = 'none';
756 | document.getElementById('edit-weekly-config').style.display = 'none';
757 |
758 | // 显示对应配置
759 | if (type) {
760 | document.getElementById(`edit-${type}-config`).style.display = 'block';
761 | }
762 | });
763 |
764 | // 取消按钮
765 | cancelBtn?.addEventListener('click', () => {
766 | this.showTaskManager(); // 返回任务管理界面
767 | });
768 |
769 | // 表单提交
770 | form?.addEventListener('submit', async (e) => {
771 | e.preventDefault();
772 | await this.handleEditTaskSubmit(taskId);
773 | });
774 | }
775 |
776 | /**
777 | * 处理编辑任务表单提交
778 | */
779 | async handleEditTaskSubmit(taskId) {
780 | try {
781 | const scriptId = parseInt(document.getElementById('edit-task-script-id').value);
782 | const taskName = document.getElementById('edit-task-name').value.trim();
783 | const scheduleType = document.getElementById('edit-schedule-type').value;
784 |
785 | if (!scriptId || !scheduleType) {
786 | this.showNotification('请填写所有必填字段', 'warning');
787 | return;
788 | }
789 |
790 | let schedule;
791 | switch (scheduleType) {
792 | case 'interval':
793 | const intervalValue = parseInt(document.getElementById('edit-interval-value').value);
794 | const intervalUnit = document.getElementById('edit-interval-unit').value;
795 | const milliseconds = intervalUnit === 'hours' ? intervalValue * 3600000 : intervalValue * 60000;
796 | schedule = { type: 'interval', value: milliseconds };
797 | break;
798 |
799 | case 'daily':
800 | const dailyTime = document.getElementById('edit-daily-time').value;
801 | schedule = { type: 'daily', time: dailyTime };
802 | break;
803 |
804 | case 'weekly':
805 | const weeklyDay = parseInt(document.getElementById('edit-weekly-day').value);
806 | const weeklyTime = document.getElementById('edit-weekly-time').value;
807 | schedule = { type: 'weekly', day: weeklyDay, time: weeklyTime };
808 | break;
809 |
810 | default:
811 | this.showNotification('不支持的调度类型', 'error');
812 | return;
813 | }
814 |
815 | const updates = {
816 | scriptId,
817 | schedule
818 | };
819 |
820 | if (taskName) {
821 | updates.name = taskName;
822 | }
823 |
824 | const result = await window.electronAPI.updateTask(taskId, updates);
825 | if (result.success) {
826 | this.showNotification('任务修改成功', 'success');
827 | // 返回任务管理界面并刷新列表
828 | this.showTaskManager();
829 |
830 | // 刷新脚本管理器的脚本卡片,显示最新的定时任务状态
831 | if (window.scriptManager) {
832 | window.scriptManager.refreshScripts();
833 | }
834 | } else {
835 | this.showNotification('修改任务失败: ' + result.error, 'error');
836 | }
837 |
838 | } catch (error) {
839 | console.error('修改任务失败:', error);
840 | this.showNotification('修改任务失败: ' + error.message, 'error');
841 | }
842 | }
843 |
844 | /**
845 | * 删除任务
846 | */
847 | async deleteTask(taskId) {
848 | // 使用ScriptManager的确认对话框
849 | if (window.scriptManager) {
850 | const confirmed = await window.scriptManager.showConfirmDialog('确认删除', '确定要删除这个任务吗?');
851 | if (!confirmed) return;
852 | } else {
853 | // 降级到原生确认框
854 | if (!confirm('确定要删除这个任务吗?')) return;
855 | }
856 |
857 | try {
858 | const result = await window.electronAPI.deleteTask(taskId);
859 | if (result.success) {
860 | this.showNotification('任务已删除', 'success');
861 | this.refreshTaskList();
862 |
863 | // 刷新脚本管理器的脚本卡片,显示最新的定时任务状态
864 | if (window.scriptManager) {
865 | window.scriptManager.refreshScripts();
866 | }
867 | } else {
868 | this.showNotification('删除失败: ' + result.error, 'error');
869 | }
870 | } catch (error) {
871 | this.showNotification('删除失败: ' + error.message, 'error');
872 | }
873 | }
874 |
875 | /**
876 | * 显示通知
877 | */
878 | showNotification(message, type = 'info') {
879 | // 使用现有的通知系统
880 | if (window.scriptManager && window.scriptManager.showNotification) {
881 | window.scriptManager.showNotification(message, type);
882 | } else {
883 | // 创建简单的通知
884 | this.createSimpleNotification(message, type);
885 | }
886 | }
887 |
888 | /**
889 | * 创建简单通知
890 | */
891 | createSimpleNotification(message, type) {
892 | // 创建通知容器(如果不存在)
893 | let container = document.querySelector('.notification-container');
894 | if (!container) {
895 | container = document.createElement('div');
896 | container.className = 'notification-container';
897 | document.body.appendChild(container);
898 | }
899 |
900 | // 创建通知元素
901 | const notification = document.createElement('div');
902 | notification.className = `notification ${type}`;
903 | notification.textContent = message;
904 |
905 | // 添加到容器
906 | container.appendChild(notification);
907 |
908 | // 3秒后自动移除
909 | setTimeout(() => {
910 | if (notification.parentNode) {
911 | notification.parentNode.removeChild(notification);
912 | }
913 | }, 3000);
914 | }
915 |
916 | /**
917 | * 为脚本显示定时任务设置
918 | */
919 | async showTaskSettingsForScript(scriptId) {
920 | this.currentScript = scriptId;
921 |
922 | // 获取该脚本的任务
923 | const result = await window.electronAPI.getTasksByScript(scriptId);
924 | if (result.success) {
925 | const tasks = result.tasks;
926 |
927 | if (tasks.length === 0) {
928 | // 没有任务,显示创建界面
929 | this.showCreateTaskDialog();
930 | } else {
931 | // 有任务,显示任务列表
932 | this.showTaskManager();
933 | }
934 | }
935 | }
936 | }
937 |
938 | // 导出给全局使用
939 | window.TaskManager = TaskManager;
940 |
--------------------------------------------------------------------------------
/assets/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmhm2022/scripts-manager/9586fc9647665f956d406ec7969a5e785d7692a4/assets/icon-128.png
--------------------------------------------------------------------------------
/assets/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmhm2022/scripts-manager/9586fc9647665f956d406ec7969a5e785d7692a4/assets/icon-16.png
--------------------------------------------------------------------------------
/assets/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmhm2022/scripts-manager/9586fc9647665f956d406ec7969a5e785d7692a4/assets/icon-256.png
--------------------------------------------------------------------------------
/assets/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmhm2022/scripts-manager/9586fc9647665f956d406ec7969a5e785d7692a4/assets/icon-32.png
--------------------------------------------------------------------------------
/assets/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmhm2022/scripts-manager/9586fc9647665f956d406ec7969a5e785d7692a4/assets/icon-64.png
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmhm2022/scripts-manager/9586fc9647665f956d406ec7969a5e785d7692a4/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmhm2022/scripts-manager/9586fc9647665f956d406ec7969a5e785d7692a4/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/build.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | echo Setting up mirrors...
3 | set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
4 | set ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
5 |
6 | echo Building portable version...
7 | npm run build-portable
8 |
9 | echo Build completed!
10 | pause
11 |
--------------------------------------------------------------------------------
/convert-icon.js:
--------------------------------------------------------------------------------
1 | const sharp = require('sharp');
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | // 确保 assets 目录存在
6 | const assetsDir = path.join(__dirname, 'assets');
7 | if (!fs.existsSync(assetsDir)) {
8 | fs.mkdirSync(assetsDir, { recursive: true });
9 | }
10 |
11 | const svgPath = path.join(assetsDir, 'icon.svg');
12 | const pngPath = path.join(assetsDir, 'icon.png');
13 | const icoPaths = [
14 | { path: path.join(assetsDir, 'icon.ico'), size: 256 },
15 | { path: path.join(assetsDir, 'icon-16.png'), size: 16 },
16 | { path: path.join(assetsDir, 'icon-32.png'), size: 32 },
17 | { path: path.join(assetsDir, 'icon-64.png'), size: 64 },
18 | { path: path.join(assetsDir, 'icon-128.png'), size: 128 },
19 | { path: path.join(assetsDir, 'icon-256.png'), size: 256 }
20 | ];
21 |
22 | // 检查 SVG 文件是否存在
23 | if (!fs.existsSync(svgPath)) {
24 | console.error('SVG 图标文件不存在:', svgPath);
25 | process.exit(1);
26 | }
27 |
28 | // 转换为 PNG (确保主图标是256x256)
29 | console.log('正在将 SVG 转换为 PNG...');
30 | sharp(svgPath)
31 | .resize(256, 256) // 确保主图标是256x256
32 | .png()
33 | .toFile(pngPath)
34 | .then(() => {
35 | console.log('PNG 图标已创建:', pngPath);
36 |
37 | // 创建不同尺寸的图标
38 | return Promise.all(
39 | icoPaths.map(({ path, size }) => {
40 | console.log(`正在创建 ${size}x${size} 图标...`);
41 | return sharp(svgPath)
42 | .resize(size, size)
43 | .png()
44 | .toFile(path)
45 | .then(() => {
46 | console.log(`${size}x${size} 图标已创建:`, path);
47 | });
48 | })
49 | );
50 | })
51 | .catch(err => {
52 | console.error('转换图标时出错:', err);
53 | });
--------------------------------------------------------------------------------
/example_scripts/hello.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /**
3 | * 示例JavaScript脚本
4 | * 演示基本的输出功能和异步操作
5 | */
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const os = require('os');
10 |
11 | // 延迟函数
12 | function delay(ms) {
13 | return new Promise(resolve => setTimeout(resolve, ms));
14 | }
15 |
16 | // 主函数
17 | async function main() {
18 | console.log("=".repeat(50));
19 | console.log("🚀 JavaScript脚本执行示例");
20 | console.log("=".repeat(50));
21 |
22 | // 显示基本信息
23 | console.log(`📅 当前时间: ${new Date().toLocaleString()}`);
24 | console.log(`🟨 Node.js版本: ${process.version}`);
25 | console.log(`💻 操作系统: ${os.type()} ${os.release()}`);
26 | console.log(`📁 当前目录: ${process.cwd()}`);
27 |
28 | // 演示异步操作
29 | console.log("\n🔄 模拟异步处理过程:");
30 | for (let i = 1; i <= 5; i++) {
31 | console.log(` 步骤 ${i}/5: 正在处理...`);
32 | await delay(300);
33 | }
34 |
35 | // 演示文件系统操作
36 | console.log("\n📂 检查文件系统:");
37 | try {
38 | const currentDir = process.cwd();
39 | const files = fs.readdirSync(currentDir);
40 | console.log(` 当前目录包含 ${files.length} 个项目`);
41 |
42 | // 显示前几个文件/目录
43 | const displayFiles = files.slice(0, 3);
44 | displayFiles.forEach(file => {
45 | const filePath = path.join(currentDir, file);
46 | const stats = fs.statSync(filePath);
47 | const type = stats.isDirectory() ? '📁' : '📄';
48 | console.log(` ${type} ${file}`);
49 | });
50 |
51 | if (files.length > 3) {
52 | console.log(` ... 还有 ${files.length - 3} 个项目`);
53 | }
54 | } catch (error) {
55 | console.log(` ❌ 读取目录失败: ${error.message}`);
56 | }
57 |
58 | // 演示环境变量
59 | console.log("\n🌍 环境信息:");
60 | console.log(` 用户名: ${os.userInfo().username}`);
61 | console.log(` 主目录: ${os.homedir()}`);
62 | console.log(` CPU架构: ${os.arch()}`);
63 |
64 | console.log("\n✅ 处理完成!");
65 | console.log("📝 这是一个演示JavaScript功能的Node.js脚本");
66 | console.log("🎉 脚本执行成功!");
67 |
68 | return 0;
69 | }
70 |
71 | // 错误处理
72 | process.on('uncaughtException', (error) => {
73 | console.error('\n❌ 未捕获的异常:', error.message);
74 | process.exit(1);
75 | });
76 |
77 | process.on('unhandledRejection', (reason, promise) => {
78 | console.error('\n❌ 未处理的Promise拒绝:', reason);
79 | process.exit(1);
80 | });
81 |
82 | // 优雅退出处理
83 | process.on('SIGINT', () => {
84 | console.log('\n⚠️ 脚本被用户中断');
85 | process.exit(1);
86 | });
87 |
88 | // 执行主函数
89 | main()
90 | .then(exitCode => {
91 | process.exit(exitCode);
92 | })
93 | .catch(error => {
94 | console.error('\n❌ 脚本执行出错:', error.message);
95 | process.exit(1);
96 | });
97 |
--------------------------------------------------------------------------------
/example_scripts/hello.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | 示例Python脚本
6 | 演示基本的输出功能和中文支持
7 | """
8 |
9 | import sys
10 | import time
11 | from datetime import datetime
12 |
13 | def main():
14 | print("=" * 50)
15 | print("🐍 Python脚本执行示例")
16 | print("=" * 50)
17 |
18 | # 显示基本信息
19 | print(f"📅 当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
20 | print(f"🐍 Python版本: {sys.version}")
21 | print(f"💻 平台信息: {sys.platform}")
22 |
23 | # 演示进度输出
24 | print("\n🔄 模拟处理过程:")
25 | for i in range(1, 6):
26 | print(f" 步骤 {i}/5: 正在处理...")
27 | time.sleep(0.5)
28 | input("按回车键继续...")
29 | # 演示中文输出
30 | print("\n✅ 处理完成!")
31 | print("📝 这是一个演示中文输出的Python脚本")
32 | print("🎉 脚本执行成功!")
33 |
34 | return 0
35 |
36 | if __name__ == "__main__":
37 | try:
38 | exit_code = main()
39 | sys.exit(exit_code)
40 | except KeyboardInterrupt:
41 | print("\n⚠️ 脚本被用户中断")
42 | sys.exit(1)
43 | except Exception as e:
44 | print(f"\n❌ 脚本执行出错: {e}")
45 | sys.exit(1)
46 |
--------------------------------------------------------------------------------
/example_scripts/test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 测试脚本 - 用于验证脚本管理器的执行功能
3 | */
4 |
5 | function main() {
6 | console.log("=".repeat(50));
7 | console.log("测试脚本执行");
8 | console.log("=".repeat(50));
9 | console.log(`当前时间: ${new Date()}`);
10 | console.log(`Node.js版本: ${process.version}`);
11 | console.log(`当前工作目录: ${process.cwd()}`);
12 | console.log(`脚本路径: ${__filename}`);
13 | console.log("=".repeat(50));
14 | console.log("测试成功!");
15 | console.log("=".repeat(50));
16 |
17 | // 添加无限循环
18 | setTimeout(main, 1000); // 每秒执行一次
19 | }
20 |
21 | main();
22 |
--------------------------------------------------------------------------------
/example_scripts/test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | 测试脚本 - 用于验证脚本管理器的执行功能
6 | """
7 |
8 | import sys
9 | import os
10 | import datetime
11 | import time
12 |
13 | def main():
14 | while True:
15 | print("Hello, world!")
16 | time.sleep(1) # 可选:暂停1秒,避免CPU占用过高
17 | print("=" * 50)
18 | print("测试脚本执行")
19 | print("=" * 50)
20 | print(f"当前时间: {datetime.datetime.now()}")
21 | print(f"Python版本: {sys.version}")
22 | print(f"当前工作目录: {os.getcwd()}")
23 | print(f"脚本路径: {__file__}")
24 | print("=" * 50)
25 | print("测试成功!")
26 | print("=" * 50)
27 | time.sleep(1)
28 |
29 | if __name__ == "__main__":
30 | main()
31 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow, ipcMain, dialog, Tray, Menu, shell } = require('electron');
2 | const path = require('path');
3 | const fs = require('fs');
4 | const fsPromises = require('fs').promises;
5 | const os = require('os');
6 | const { exec: execCommand } = require('child_process');
7 |
8 | // 设置控制台编码为UTF-8,解决中文显示问题
9 | if (process.platform === 'win32') {
10 | try {
11 | process.stdout.setEncoding('utf8');
12 | process.stderr.setEncoding('utf8');
13 | // 设置Windows控制台代码页为UTF-8
14 | execCommand('chcp 65001', { encoding: 'utf8' }, (error) => {
15 | if (error) {
16 | console.log('设置控制台编码失败,但不影响应用运行');
17 | }
18 | });
19 | } catch (error) {
20 | // 忽略编码设置错误
21 | }
22 | }
23 |
24 | // 在应用启动前进行全面的错误抑制配置
25 | app.commandLine.appendSwitch('--disable-gpu');
26 | app.commandLine.appendSwitch('--disable-gpu-sandbox');
27 | app.commandLine.appendSwitch('--disable-software-rasterizer');
28 | app.commandLine.appendSwitch('--disable-gpu-memory-buffer-compositor-resources');
29 | app.commandLine.appendSwitch('--disable-gpu-memory-buffer-video-frames');
30 | app.commandLine.appendSwitch('--disable-background-timer-throttling');
31 | app.commandLine.appendSwitch('--disable-backgrounding-occluded-windows');
32 | app.commandLine.appendSwitch('--disable-renderer-backgrounding');
33 | app.commandLine.appendSwitch('--disable-features=TranslateUI');
34 | app.commandLine.appendSwitch('--disable-ipc-flooding-protection');
35 | app.commandLine.appendSwitch('--no-sandbox');
36 | app.commandLine.appendSwitch('--disable-gpu-process-crash-limit');
37 | app.commandLine.appendSwitch('--disable-gl-drawing-for-tests');
38 | app.commandLine.appendSwitch('--disable-accelerated-2d-canvas');
39 | app.commandLine.appendSwitch('--disable-accelerated-jpeg-decoding');
40 | app.commandLine.appendSwitch('--disable-accelerated-mjpeg-decode');
41 | app.commandLine.appendSwitch('--disable-accelerated-video-decode');
42 | app.commandLine.appendSwitch('--disable-accelerated-video-encode');
43 | app.commandLine.appendSwitch('--disable-gpu-rasterization');
44 | app.commandLine.appendSwitch('--disable-gpu-compositing');
45 | app.commandLine.appendSwitch('--disable-3d-apis');
46 | app.commandLine.appendSwitch('--disable-webgl');
47 | app.commandLine.appendSwitch('--disable-webgl2');
48 | app.commandLine.appendSwitch('--use-gl=swiftshader');
49 | app.commandLine.appendSwitch('--ignore-gpu-blacklist');
50 | app.commandLine.appendSwitch('--disable-dev-shm-usage');
51 |
52 | // 禁用GPU加速以避免GPU进程错误
53 | app.disableHardwareAcceleration();
54 |
55 | // 设置持久化的用户数据目录,避免中文路径问题
56 | const userDataPath = path.join(os.homedir(), '.script-manager');
57 | app.setPath('userData', userDataPath);
58 | app.setPath('temp', path.join(userDataPath, 'temp'));
59 | app.setPath('cache', path.join(userDataPath, 'cache'));
60 |
61 | // 确保用户数据目录存在
62 | if (!fs.existsSync(userDataPath)) {
63 | fs.mkdirSync(userDataPath, { recursive: true });
64 | }
65 |
66 | // 抑制控制台错误输出
67 | const originalConsoleError = console.error;
68 | console.error = (...args) => {
69 | const message = args.join(' ');
70 | // 过滤掉已知的无害错误
71 | if (message.includes('cache_util_win.cc') ||
72 | message.includes('gpu_disk_cache.cc') ||
73 | message.includes('disk_cache.cc') ||
74 | message.includes('Unable to move the cache') ||
75 | message.includes('Gpu Cache Creation failed') ||
76 | message.includes('gpu_channel_manager.cc') ||
77 | message.includes('Failed to create GLES3 context') ||
78 | message.includes('Failed to create shared context for virtualization') ||
79 | message.includes('ContextResult::kFatalFailure') ||
80 | message.includes('fallback to GLES2')) {
81 | return; // 忽略这些错误
82 | }
83 | originalConsoleError.apply(console, args);
84 | };
85 |
86 | // 导入业务逻辑模块
87 | const ScriptManager = require('./app/main/script-manager');
88 | const ScriptExecutor = require('./app/main/script-executor');
89 | const FileManager = require('./app/main/file-manager');
90 | const TaskScheduler = require('./app/main/task-scheduler');
91 | const SettingsManager = require('./app/main/settings-manager');
92 |
93 | class ScriptManagerApp {
94 | constructor() {
95 | // 检查单实例锁定
96 | const gotTheLock = app.requestSingleInstanceLock();
97 |
98 | if (!gotTheLock) {
99 | console.log('脚本管理器已在运行,退出当前实例');
100 | app.quit();
101 | return;
102 | }
103 |
104 | console.log('获取单实例锁成功,继续启动应用');
105 |
106 | this.mainWindow = null;
107 | this.tray = null;
108 | this.scriptManager = new ScriptManager();
109 | this.scriptExecutor = new ScriptExecutor();
110 | this.fileManager = new FileManager();
111 | this.settingsManager = new SettingsManager();
112 | this.taskScheduler = new TaskScheduler(this.scriptExecutor, this.scriptManager);
113 |
114 | this.initializeApp();
115 | }
116 |
117 | initializeApp() {
118 | // 处理第二个实例启动的情况
119 | app.on('second-instance', (event, commandLine, workingDirectory) => {
120 | console.log('检测到第二个实例启动,激活现有窗口');
121 | this.activateMainWindow();
122 | });
123 |
124 | // 当Electron完成初始化时创建窗口
125 | app.whenReady().then(() => {
126 | this.createWindow();
127 | this.createTray();
128 | this.setupIPC();
129 | });
130 |
131 | // 添加退出前的处理逻辑
132 | app.on('before-quit', () => {
133 | app.isQuitting = true;
134 | });
135 |
136 | // 当所有窗口关闭时退出应用(macOS除外)
137 | app.on('window-all-closed', () => {
138 | if (process.platform !== 'darwin') {
139 | app.quit();
140 | }
141 | });
142 |
143 | // macOS激活应用时重新创建窗口
144 | app.on('activate', () => {
145 | if (BrowserWindow.getAllWindows().length === 0) {
146 | this.createWindow();
147 | }
148 | });
149 | }
150 |
151 | // 激活主窗口的方法
152 | activateMainWindow() {
153 | try {
154 | if (this.mainWindow && !this.mainWindow.isDestroyed()) {
155 | // 如果窗口存在但被隐藏,显示它
156 | if (!this.mainWindow.isVisible()) {
157 | this.mainWindow.show();
158 | }
159 |
160 | // 如果窗口被最小化,恢复它
161 | if (this.mainWindow.isMinimized()) {
162 | this.mainWindow.restore();
163 | }
164 |
165 | // 将窗口置于前台并获得焦点
166 | this.mainWindow.focus();
167 |
168 | // 在 Windows 上额外处理,确保窗口真正获得焦点
169 | if (process.platform === 'win32') {
170 | this.mainWindow.setAlwaysOnTop(true);
171 | this.mainWindow.setAlwaysOnTop(false);
172 | }
173 |
174 | console.log('已激活现有的脚本管理器窗口');
175 | } else {
176 | // 如果窗口不存在或已被销毁,创建新窗口
177 | console.log('主窗口不存在或已销毁,创建新窗口');
178 | this.createWindow();
179 | }
180 | } catch (error) {
181 | console.error('激活主窗口时发生错误:', error);
182 | // 发生错误时尝试创建新窗口
183 | try {
184 | this.createWindow();
185 | } catch (createError) {
186 | console.error('创建新窗口失败:', createError);
187 | }
188 | }
189 | }
190 |
191 | createWindow() {
192 | // 创建主窗口
193 | this.mainWindow = new BrowserWindow({
194 | width: 1200,
195 | height: 800,
196 | minWidth: 800,
197 | minHeight: 600,
198 | webPreferences: {
199 | nodeIntegration: false,
200 | contextIsolation: true,
201 | preload: path.join(__dirname, 'preload.js'),
202 | webSecurity: true,
203 | enableRemoteModule: false,
204 | allowRunningInsecureContent: false,
205 | experimentalFeatures: false,
206 | backgroundThrottling: false,
207 | offscreen: false,
208 | sandbox: false,
209 | spellcheck: false
210 | },
211 | title: '脚本管理器',
212 | show: false, // 先隐藏,加载完成后显示
213 | autoHideMenuBar: true, // 隐藏菜单栏
214 | icon: path.join(__dirname, 'assets/icon.png'), // 使用新的图标
215 | frame: true,
216 | resizable: true,
217 | maximizable: true,
218 | minimizable: true,
219 | closable: true
220 | });
221 |
222 | // 加载应用界面
223 | this.mainWindow.loadFile('app/renderer/index.html');
224 |
225 | // 窗口准备好后显示
226 | this.mainWindow.once('ready-to-show', () => {
227 | this.mainWindow.show();
228 | });
229 |
230 | // 开发模式下打开开发者工具
231 | if (process.argv.includes('--dev')) {
232 | this.mainWindow.webContents.openDevTools();
233 | }
234 |
235 | // 处理窗口关闭事件
236 | this.mainWindow.on('close', async (event) => {
237 | // 获取设置
238 | const settingResult = await this.settingsManager.getSetting('minimizeToTray');
239 | const minimizeToTray = settingResult.success && settingResult.value === true;
240 |
241 | if (minimizeToTray && !app.isQuitting) {
242 | event.preventDefault();
243 | this.mainWindow.hide();
244 | return false;
245 | }
246 |
247 | // 只有在真正关闭窗口时才设置为 null
248 | if (app.isQuitting) {
249 | this.mainWindow = null;
250 | }
251 | });
252 | }
253 |
254 | setupIPC() {
255 | // 脚本管理相关IPC
256 | ipcMain.handle('load-scripts', async () => {
257 | try {
258 | return await this.scriptManager.loadScripts();
259 | } catch (error) {
260 | console.error('加载脚本失败:', error);
261 | return { success: false, error: error.message };
262 | }
263 | });
264 |
265 | ipcMain.handle('save-script', async (event, scriptData) => {
266 | try {
267 | return await this.scriptManager.saveScript(scriptData);
268 | } catch (error) {
269 | console.error('保存脚本失败:', error);
270 | return { success: false, error: error.message };
271 | }
272 | });
273 |
274 | ipcMain.handle('update-script', async (event, scriptId, scriptData) => {
275 | try {
276 | return await this.scriptManager.updateScript(scriptId, scriptData);
277 | } catch (error) {
278 | console.error('更新脚本失败:', error);
279 | return { success: false, error: error.message };
280 | }
281 | });
282 |
283 | ipcMain.handle('delete-script', async (event, scriptId) => {
284 | try {
285 | // 先删除脚本
286 | const scriptResult = await this.scriptManager.deleteScript(scriptId);
287 | if (!scriptResult.success) {
288 | return scriptResult;
289 | }
290 |
291 | // 删除脚本成功后,清理相关的定时任务
292 | const taskResult = await this.taskScheduler.deleteTasksByScript(scriptId);
293 |
294 | return {
295 | success: true,
296 | message: '脚本删除成功',
297 | taskCleanup: taskResult
298 | };
299 | } catch (error) {
300 | console.error('删除脚本失败:', error);
301 | return { success: false, error: error.message };
302 | }
303 | });
304 |
305 | // 脚本启动相关IPC(替代原来的执行)
306 | ipcMain.handle('launch-script', async (event, scriptId) => {
307 | try {
308 | const script = await this.scriptManager.getScript(scriptId);
309 | if (!script) {
310 | return { success: false, error: '脚本不存在' };
311 | }
312 | return await this.scriptExecutor.launchScript(scriptId, script);
313 | } catch (error) {
314 | console.error('启动脚本失败:', error);
315 | return { success: false, error: error.message };
316 | }
317 | });
318 |
319 | // 获取已启动的进程列表
320 | ipcMain.handle('get-launched-processes', async () => {
321 | try {
322 | return {
323 | success: true,
324 | processes: this.scriptExecutor.getLaunchedProcesses()
325 | };
326 | } catch (error) {
327 | console.error('获取进程列表失败:', error);
328 | return { success: false, error: error.message };
329 | }
330 | });
331 |
332 | // 停止特定脚本
333 | ipcMain.handle('stop-script', async (event, scriptId) => {
334 | try {
335 | return await this.scriptExecutor.stopScript(scriptId);
336 | } catch (error) {
337 | console.error('停止脚本失败:', error);
338 | return { success: false, error: error.message };
339 | }
340 | });
341 |
342 | // 清理已结束的进程记录
343 | ipcMain.handle('cleanup-processes', async () => {
344 | try {
345 | this.scriptExecutor.cleanupProcesses();
346 | return { success: true };
347 | } catch (error) {
348 | console.error('清理进程失败:', error);
349 | return { success: false, error: error.message };
350 | }
351 | });
352 |
353 | // 文件管理相关IPC
354 | ipcMain.handle('browse-file', async () => {
355 | try {
356 | // 根据平台设置文件过滤器
357 | const filters = this.getFileFiltersForPlatform();
358 |
359 | const result = await dialog.showOpenDialog(this.mainWindow, {
360 | properties: ['openFile'],
361 | filters: filters
362 | });
363 |
364 | if (!result.canceled && result.filePaths.length > 0) {
365 | return { success: true, filePath: result.filePaths[0] };
366 | }
367 | return { success: false, error: '未选择文件' };
368 | } catch (error) {
369 | console.error('浏览文件失败:', error);
370 | return { success: false, error: error.message };
371 | }
372 | });
373 |
374 | ipcMain.handle('validate-file', async (event, filePath) => {
375 | try {
376 | return await this.fileManager.validateFile(filePath);
377 | } catch (error) {
378 | console.error('验证文件失败:', error);
379 | return { success: false, error: error.message };
380 | }
381 | });
382 |
383 | ipcMain.handle('open-script-folder', async (event, scriptPath) => {
384 | try {
385 | const { shell } = require('electron');
386 |
387 | // 使用shell.showItemInFolder在文件管理器中显示文件
388 | shell.showItemInFolder(scriptPath);
389 |
390 | return { success: true };
391 | } catch (error) {
392 | console.error('打开文件夹失败:', error);
393 | return { success: false, error: error.message };
394 | }
395 | });
396 |
397 | // 定时任务相关IPC
398 | ipcMain.handle('get-tasks', async () => {
399 | try {
400 | const tasks = this.taskScheduler.getTasks();
401 | return { success: true, tasks };
402 | } catch (error) {
403 | console.error('获取任务列表失败:', error);
404 | return { success: false, error: error.message };
405 | }
406 | });
407 |
408 | ipcMain.handle('create-task', async (event, taskData) => {
409 | try {
410 | return await this.taskScheduler.createTask(taskData);
411 | } catch (error) {
412 | console.error('创建任务失败:', error);
413 | return { success: false, error: error.message };
414 | }
415 | });
416 |
417 | ipcMain.handle('update-task', async (event, taskId, updates) => {
418 | try {
419 | return await this.taskScheduler.updateTask(taskId, updates);
420 | } catch (error) {
421 | console.error('更新任务失败:', error);
422 | return { success: false, error: error.message };
423 | }
424 | });
425 |
426 | ipcMain.handle('delete-task', async (event, taskId) => {
427 | try {
428 | return await this.taskScheduler.deleteTask(taskId);
429 | } catch (error) {
430 | console.error('删除任务失败:', error);
431 | return { success: false, error: error.message };
432 | }
433 | });
434 |
435 | ipcMain.handle('toggle-task', async (event, taskId, enabled) => {
436 | try {
437 | return await this.taskScheduler.toggleTask(taskId, enabled);
438 | } catch (error) {
439 | console.error('切换任务状态失败:', error);
440 | return { success: false, error: error.message };
441 | }
442 | });
443 |
444 | ipcMain.handle('run-task-now', async (event, taskId) => {
445 | try {
446 | return await this.taskScheduler.runTaskNow(taskId);
447 | } catch (error) {
448 | console.error('立即执行任务失败:', error);
449 | return { success: false, error: error.message };
450 | }
451 | });
452 |
453 | ipcMain.handle('get-tasks-by-script', async (event, scriptId) => {
454 | try {
455 | const tasks = this.taskScheduler.getTasksByScript(scriptId);
456 | return { success: true, tasks };
457 | } catch (error) {
458 | console.error('获取脚本任务失败:', error);
459 | return { success: false, error: error.message };
460 | }
461 | });
462 |
463 | ipcMain.handle('get-scheduler-status', async () => {
464 | try {
465 | const status = this.taskScheduler.getStatus();
466 | return { success: true, status };
467 | } catch (error) {
468 | console.error('获取调度器状态失败:', error);
469 | return { success: false, error: error.message };
470 | }
471 | });
472 |
473 | // 设置相关IPC
474 | ipcMain.handle('load-settings', async () => {
475 | try {
476 | return await this.settingsManager.loadSettings();
477 | } catch (error) {
478 | console.error('加载设置失败:', error);
479 | return { success: false, error: error.message };
480 | }
481 | });
482 |
483 | ipcMain.handle('save-settings', async (event, settings) => {
484 | try {
485 | return await this.settingsManager.saveSettings(settings);
486 | } catch (error) {
487 | console.error('保存设置失败:', error);
488 | return { success: false, error: error.message };
489 | }
490 | });
491 |
492 | ipcMain.handle('get-setting', async (event, key) => {
493 | try {
494 | return await this.settingsManager.getSetting(key);
495 | } catch (error) {
496 | console.error(`获取设置 ${key} 失败:`, error);
497 | return { success: false, error: error.message };
498 | }
499 | });
500 |
501 | // 打开外部链接
502 | ipcMain.handle('open-external', async (event, url) => {
503 | try {
504 | await shell.openExternal(url);
505 | return { success: true };
506 | } catch (error) {
507 | console.error('打开外部链接失败:', error);
508 | return { success: false, error: error.message };
509 | }
510 | });
511 | }
512 |
513 | createTray() {
514 | // 创建托盘图标
515 | const nativeImage = require('electron').nativeImage;
516 | let trayIcon;
517 |
518 | try {
519 | // 首先尝试使用小尺寸图标(16x16 或 32x32 最适合托盘)
520 | let iconPath;
521 | if (process.platform === 'win32') {
522 | // Windows 平台优先使用 16x16 图标
523 | iconPath = path.join(__dirname, 'assets/icon-16.png');
524 | } else {
525 | // 其他平台使用 32x32 图标
526 | iconPath = path.join(__dirname, 'assets/icon-32.png');
527 | }
528 |
529 | console.log('尝试加载托盘图标:', iconPath);
530 |
531 | if (fs.existsSync(iconPath)) {
532 | console.log('图标文件存在,正在加载...');
533 | trayIcon = nativeImage.createFromPath(iconPath);
534 | console.log('成功加载托盘图标');
535 | } else {
536 | // 如果小尺寸图标不存在,尝试使用标准图标
537 | iconPath = path.join(__dirname, 'assets/icon.png');
538 | if (fs.existsSync(iconPath)) {
539 | console.log('使用标准图标:', iconPath);
540 | trayIcon = nativeImage.createFromPath(iconPath);
541 | // 调整大小以适合托盘
542 | trayIcon = trayIcon.resize({ width: 16, height: 16 });
543 | } else {
544 | throw new Error('找不到任何图标文件');
545 | }
546 | }
547 | } catch (error) {
548 | console.error('加载托盘图标失败:', error);
549 |
550 | // 如果加载失败,创建一个简单的图标
551 | try {
552 | console.log('创建备用图标...');
553 | const emptyIcon = nativeImage.createEmpty();
554 | emptyIcon.addRepresentation({
555 | width: 16,
556 | height: 16,
557 | buffer: Buffer.alloc(16 * 16 * 4, 255), // 创建一个16x16的白色图标
558 | scaleFactor: 1.0
559 | });
560 | trayIcon = emptyIcon;
561 | console.log('成功创建备用图标');
562 | } catch (finalErr) {
563 | console.error('创建备用图标失败:', finalErr);
564 | trayIcon = nativeImage.createEmpty();
565 | }
566 | }
567 |
568 | // 创建托盘
569 | console.log('创建系统托盘...');
570 | this.tray = new Tray(trayIcon);
571 |
572 | // 在 macOS 上设置为模板图标以获得更好的系统集成
573 | if (process.platform === 'darwin') {
574 | this.tray.setTemplateImage(true);
575 | }
576 |
577 | this.tray.setToolTip('脚本管理器');
578 | console.log('托盘创建成功');
579 |
580 | // 创建托盘菜单
581 | const contextMenu = Menu.buildFromTemplate([
582 | {
583 | label: '显示窗口',
584 | click: () => {
585 | if (this.mainWindow) {
586 | this.mainWindow.show();
587 | } else {
588 | this.createWindow();
589 | }
590 | }
591 | },
592 | {
593 | label: '退出',
594 | click: () => {
595 | app.isQuitting = true;
596 | app.quit();
597 | }
598 | }
599 | ]);
600 |
601 | this.tray.setContextMenu(contextMenu);
602 | console.log('托盘菜单设置完成');
603 |
604 | // 点击托盘图标显示窗口
605 | this.tray.on('click', () => {
606 | if (this.mainWindow) {
607 | if (this.mainWindow.isVisible()) {
608 | if (this.mainWindow.isMinimized()) {
609 | this.mainWindow.restore();
610 | }
611 | } else {
612 | this.mainWindow.show();
613 | }
614 | this.mainWindow.focus();
615 | } else {
616 | this.createWindow();
617 | }
618 | });
619 | console.log('托盘点击事件设置完成');
620 | }
621 |
622 | getFileFiltersForPlatform() {
623 | const baseFilters = [
624 | { name: 'Python脚本', extensions: ['py', 'pyw'] },
625 | { name: 'JavaScript脚本', extensions: ['js'] },
626 | { name: 'TypeScript脚本', extensions: ['ts'] },
627 | { name: 'Shell脚本', extensions: ['sh'] }
628 | ];
629 |
630 | if (process.platform === 'win32') {
631 | // Windows 平台添加特有的脚本类型
632 | baseFilters.push(
633 | { name: 'Batch脚本', extensions: ['bat', 'cmd'] },
634 | { name: 'PowerShell脚本', extensions: ['ps1'] }
635 | );
636 | } else if (process.platform === 'darwin') {
637 | // macOS 平台添加特有的脚本类型
638 | baseFilters.push(
639 | { name: 'macOS脚本', extensions: ['command', 'tool'] }
640 | );
641 | }
642 |
643 | // 添加所有文件选项
644 | baseFilters.push({ name: '所有文件', extensions: ['*'] });
645 |
646 | return baseFilters;
647 | }
648 | }
649 |
650 | // 创建应用实例
651 | new ScriptManagerApp();
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scripts-manager",
3 | "version": "1.3.9",
4 | "description": "Scripts Manager - 轻量级脚本管理工具",
5 | "main": "main.js",
6 | "scripts": {
7 | "start": "electron .",
8 | "dev": "electron . --dev",
9 | "build": "electron-builder",
10 | "build-win": "electron-builder --win",
11 | "build-linux": "electron-builder --linux",
12 | "build-mac": "electron-builder --mac",
13 | "build-portable": "electron-builder --win portable",
14 | "build-installer": "electron-builder --win nsis",
15 | "dist": "electron-builder --publish=never",
16 | "dist-all": "electron-builder --win --linux --mac --publish=never",
17 | "clean": "rimraf dist"
18 | },
19 | "keywords": [
20 | "script",
21 | "manager",
22 | "desktop",
23 | "electron"
24 | ],
25 | "author": "Scripts Manager Team",
26 | "license": "MIT",
27 | "devDependencies": {
28 | "electron": "^27.0.0",
29 | "electron-builder": "^24.6.4",
30 | "rimraf": "^6.0.1",
31 | "sharp": "^0.34.2"
32 | },
33 | "dependencies": {
34 | "iconv-lite": "^0.6.3"
35 | },
36 | "build": {
37 | "appId": "com.scriptsmanager.app",
38 | "productName": "Scripts Manager",
39 | "directories": {
40 | "output": "dist"
41 | },
42 | "files": [
43 | "main.js",
44 | "preload.js",
45 | "app/**/*",
46 | "assets/**/*",
47 | "node_modules/**/*",
48 | "package.json"
49 | ],
50 | "icon": "assets/icon-256.png",
51 | "win": {
52 | "icon": "assets/icon-256.png",
53 | "target": [
54 | {
55 | "target": "portable",
56 | "arch": [
57 | "x64"
58 | ]
59 | },
60 | {
61 | "target": "nsis",
62 | "arch": [
63 | "x64"
64 | ]
65 | }
66 | ]
67 | },
68 | "portable": {
69 | "artifactName": "ScriptsManager-${version}-portable.exe"
70 | },
71 | "nsis": {
72 | "oneClick": false,
73 | "allowToChangeInstallationDirectory": true,
74 | "artifactName": "ScriptsManager-${version}-setup.exe",
75 | "shortcutName": "Scripts Manager",
76 | "uninstallDisplayName": "Scripts Manager"
77 | },
78 | "linux": {
79 | "icon": "assets/icon-256.png",
80 | "target": [
81 | {
82 | "target": "AppImage",
83 | "arch": [
84 | "x64"
85 | ]
86 | }
87 | ]
88 | },
89 | "mac": {
90 | "icon": "assets/icon-256.png",
91 | "target": [
92 | {
93 | "target": "dmg",
94 | "arch": [
95 | "x64",
96 | "arm64"
97 | ]
98 | }
99 | ]
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/preload.js:
--------------------------------------------------------------------------------
1 | const { contextBridge, ipcRenderer } = require('electron');
2 |
3 | // 暴露安全的API给渲染进程
4 | contextBridge.exposeInMainWorld('electronAPI', {
5 | // 脚本管理API
6 | loadScripts: () => ipcRenderer.invoke('load-scripts'),
7 | saveScript: (scriptData) => ipcRenderer.invoke('save-script', scriptData),
8 | updateScript: (scriptId, scriptData) => ipcRenderer.invoke('update-script', scriptId, scriptData),
9 | deleteScript: (scriptId) => ipcRenderer.invoke('delete-script', scriptId),
10 |
11 | // 脚本启动API(替代执行)
12 | launchScript: (scriptId) => ipcRenderer.invoke('launch-script', scriptId),
13 |
14 | // 进程管理API
15 | getLaunchedProcesses: () => ipcRenderer.invoke('get-launched-processes'),
16 | stopScript: (scriptId) => ipcRenderer.invoke('stop-script', scriptId),
17 | cleanupProcesses: () => ipcRenderer.invoke('cleanup-processes'),
18 |
19 | // 文件管理API
20 | browseFile: () => ipcRenderer.invoke('browse-file'),
21 | validateFile: (filePath) => ipcRenderer.invoke('validate-file', filePath),
22 | openScriptFolder: (scriptPath) => ipcRenderer.invoke('open-script-folder', scriptPath),
23 |
24 | // 定时任务API
25 | getTasks: () => ipcRenderer.invoke('get-tasks'),
26 | createTask: (taskData) => ipcRenderer.invoke('create-task', taskData),
27 | updateTask: (taskId, updates) => ipcRenderer.invoke('update-task', taskId, updates),
28 | deleteTask: (taskId) => ipcRenderer.invoke('delete-task', taskId),
29 | toggleTask: (taskId, enabled) => ipcRenderer.invoke('toggle-task', taskId, enabled),
30 | runTaskNow: (taskId) => ipcRenderer.invoke('run-task-now', taskId),
31 | getTasksByScript: (scriptId) => ipcRenderer.invoke('get-tasks-by-script', scriptId),
32 | getSchedulerStatus: () => ipcRenderer.invoke('get-scheduler-status'),
33 |
34 | // 设置相关API
35 | loadSettings: () => ipcRenderer.invoke('load-settings'),
36 | saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
37 | getSetting: (key) => ipcRenderer.invoke('get-setting', key),
38 |
39 | // 工具函数
40 | platform: process.platform,
41 | version: process.versions.electron,
42 | openExternal: (url) => ipcRenderer.invoke('open-external', url)
43 | });
--------------------------------------------------------------------------------