diff --git a/.gitignore b/.gitignore index b883a13..53f8bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ obj/ .vs/ *.user *.suo +artifacts/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..f422e34 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,280 @@ +# OmniScheduler 开发指导 + +本文档面向参与 OmniScheduler 开发、构建和发布的维护者。 + +## 技术栈 + +- 语言:C# +- 运行时:.NET 8 +- 框架:WPF +- 目标平台:Windows +- 项目类型:WinExe +- 项目文件:`OmniScheduler/OmniScheduler.csproj` + +项目启用了: + +- `UseWPF` +- `UseWindowsForms` + +其中 Windows Forms 主要用于系统托盘 `NotifyIcon`。 + +## 目录结构 + +```text +. +├── OmniScheduler/ +│ ├── App.xaml +│ ├── MainWindow.xaml +│ ├── MainWindow.xaml.cs +│ ├── Models.cs +│ ├── Services.cs +│ ├── SettingsWindow.xaml +│ ├── SettingsWindow.xaml.cs +│ ├── TaskEditorWindow.xaml +│ ├── TaskEditorWindow.xaml.cs +│ └── OmniScheduler.csproj +├── app.ico +├── README.md +├── DEVELOPMENT.md +└── PRD.md +``` + +## 主要模块 + +### `Models.cs` + +定义核心数据模型: + +- `SchedulerState`:应用整体状态,包含任务、日志和设置。 +- `AppSettings`:全局设置,例如 OmniNotify API 地址和日志保留策略。 +- `ScheduledTask`:单个调度任务。 +- `TaskTrigger`:任务触发器。 +- `ExecutionLog`:执行日志。 + +触发器类型由 `TriggerKind` 定义: + +- `OneTime` +- `Interval` +- `Daily` +- `Weekly` +- `Monthly` +- `Cron` + +### `Services.cs` + +包含应用服务层: + +- `StateStore`:负责读取和保存 `%LOCALAPPDATA%\OmniScheduler\state.json`。 +- `NotifyClient`:负责向 OmniNotify API 发送 JSON 请求。 +- `SchedulerService`:调度循环、任务触发、手动触发和补偿策略处理。 +- `NextRunCalculator`:计算下次执行时间和未来执行预览。 + +### `MainWindow` + +主界面,负责: + +- 任务列表展示 +- 日志列表展示 +- 新建、编辑、删除、克隆、手动触发任务 +- 全局暂停/恢复 +- 托盘菜单与隐藏窗口行为 + +### `TaskEditorWindow` + +任务编辑窗口,包含三个页签: + +- 常规 +- 触发器 +- 消息动作 + +触发器页会根据 `TriggerKind` 动态切换配置区域。时间输入应优先使用日期选择器、下拉框、按钮等结构化控件,避免要求用户手写时间格式。 + +### `SettingsWindow` + +全局设置窗口。当前支持: + +- OmniNotify API 地址 +- 日志保留天数 +- 最大日志条数 + +为了降低对用户系统的影响,项目不提供开机自启功能,也不写入 Windows 启动项。 + +## 本地开发 + +### 环境要求 + +- Windows +- .NET 8 SDK +- 支持 WPF 的开发环境,例如 Visual Studio、Rider 或 VS Code + .NET SDK + +### 还原与构建 + +```powershell +dotnet restore .\OmniScheduler\OmniScheduler.csproj +dotnet build .\OmniScheduler\OmniScheduler.csproj +``` + +如果本地正在运行 `OmniScheduler.exe`,构建可能因为 `bin` 目录文件被锁定而失败。可以先退出应用,或临时输出到其他目录: + +```powershell +dotnet build .\OmniScheduler\OmniScheduler.csproj -p:OutDir=.\obj\codex-verify\ +``` + +### 运行 + +```powershell +dotnet run --project .\OmniScheduler\OmniScheduler.csproj +``` + +也可以直接运行构建后的 exe。 + +## 数据存储 + +运行时数据保存在: + +```text +%LOCALAPPDATA%\OmniScheduler\state.json +``` + +`StateStore` 会在文件不存在或读取失败时创建默认状态。默认任务用于 OmniNotify 连通性测试,并保持禁用,避免首次启动自动发送消息。 + +## 调度规则开发注意事项 + +- 所有下次执行时间计算应集中在 `NextRunCalculator`。 +- UI 里的触发器摘要来自 `TaskTrigger.Summary`。 +- 修改触发器模型时,需要同步检查: + - 克隆逻辑:`TaskTrigger.Clone` + - 摘要逻辑:`TaskTrigger.Summary` + - 下次执行计算:`NextRunCalculator.NextRun` + - 任务编辑窗口加载与保存:`TaskEditorWindow` +- 对用户可见的时间输入,优先使用结构化控件,不新增要求用户记忆格式的文本输入。 + +## OmniNotify API 约定 + +`NotifyClient` 发送 `POST` 请求,Content-Type 为 `application/json`: + +```json +{ + "channel": "default", + "title": "标题", + "body": "内容" +} +``` + +发送前会替换以下变量: + +- `{CurrentTime}` +- `{TaskName}` +- `{TriggerType}` + +响应状态码非 2xx 时,日志级别记为 `ERROR`。如果响应中包含 `IllegalChannel`,错误消息会提示检查频道名。 + +## 托盘图标 + +项目图标由根目录 `app.ico` 提供: + +- `ApplicationIcon` 用于嵌入 exe 图标。 +- `Resource Include="..\app.ico"` 用于 WPF 窗口图标资源。 +- 托盘图标从当前进程 exe 的关联图标读取,不依赖发布目录中的独立 `app.ico` 文件。 + +发布包中不应包含外置 `app.ico`。 + +## 发布构建 + +首次发布使用 framework-dependent、多文件包,不内置运行时,不启用 single-file: + +```powershell +dotnet publish .\OmniScheduler\OmniScheduler.csproj ` + -c Release ` + -r win-x64 ` + --self-contained false ` + -p:PublishSingleFile=false ` + -p:PublishReadyToRun=false ` + -p:DebugType=none ` + -p:DebugSymbols=false ` + -p:PublishDir=..\artifacts\OmniScheduler-v0.1.0-win-x64\ +``` + +压缩包命名建议遵循 GitHub Release 常见格式: + +```text +OmniScheduler-v0.1.0-win-x64.zip +``` + +压缩: + +```powershell +Compress-Archive ` + -Path .\artifacts\OmniScheduler-v0.1.0-win-x64 ` + -DestinationPath .\artifacts\OmniScheduler-v0.1.0-win-x64.zip +``` + +计算校验值: + +```powershell +Get-FileHash .\artifacts\OmniScheduler-v0.1.0-win-x64.zip -Algorithm SHA256 +``` + +发布包预期包含: + +```text +OmniScheduler.exe +OmniScheduler.dll +OmniScheduler.deps.json +OmniScheduler.runtimeconfig.json +``` + +## Git 与提交规范 + +提交信息使用 Conventional Commits: + +```text +[optional scope]: + +[optional body] +``` + +示例: + +```text +feat: 优化触发器配置体验 + +- 根据触发器类型动态展示对应配置区域。 +- 增加单次执行的延后快捷设置。 +``` + +常用 type: + +- `feat`:新增功能 +- `fix`:修复问题 +- `docs`:文档变更 +- `refactor`:重构 +- `chore`:构建、发布、工具或维护性变更 + +## 版本标签 + +创建版本标签: + +```powershell +git tag -a v0.1.0 -m "v0.1.0" +git push origin v0.1.0 +``` + +如果发布内容修正后需要移动尚未正式发布的标签,应明确确认后再执行: + +```powershell +git tag -fa v0.1.0 -m "v0.1.0" +git push --force origin v0.1.0 +``` + +## 质量检查清单 + +提交前建议确认: + +- `dotnet build` 通过。 +- 触发器编辑窗口能正常切换各触发类型。 +- 单次执行快捷设置能正确回填日期时间。 +- Cron 预览可显示未来执行时间或明确错误提示。 +- 发送测试消息不会要求保存任务。 +- 发布包不包含 `app.ico`、PDB、运行时目录或其他多余文件。 +- 程序关闭后能隐藏到托盘,托盘菜单可打开、暂停和退出。 diff --git a/OmniScheduler/MainWindow.xaml.cs b/OmniScheduler/MainWindow.xaml.cs index 6515230..af96a43 100644 --- a/OmniScheduler/MainWindow.xaml.cs +++ b/OmniScheduler/MainWindow.xaml.cs @@ -66,10 +66,10 @@ public partial class MainWindow : Window, INotifyPropertyChanged }); menu.Items.Add("退出", null, (_, _) => ExitApplication()); - var iconPath = System.IO.Path.Combine(AppContext.BaseDirectory, "app.ico"); var icon = new Forms.NotifyIcon { - Icon = new System.Drawing.Icon(iconPath), + Icon = System.Drawing.Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? string.Empty) + ?? System.Drawing.SystemIcons.Application, Text = "OmniScheduler", Visible = true, ContextMenuStrip = menu diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbcb504 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# OmniScheduler + +OmniScheduler 是一款面向 Windows 的本地任务调度与消息推送工具。你可以创建定时、间隔、每周、每月或 Cron 规则,在规则触发时向本机 OmniNotify API 发送结构化通知消息。 + +它适合用来做本地提醒、周期性消息推送、脚本外部通知入口,以及需要可视化管理触发规则的轻量自动化场景。 + +## 功能概览 + +- 任务管理:创建、编辑、删除、克隆、启用/禁用任务。 +- 多触发器:一个任务可以配置多个触发器。 +- 触发类型:支持单次执行、固定间隔、每日定时、每周定时、每月定时、Cron 表达式。 +- 便捷时间设置:通过日期选择器和时分秒下拉框设置时间,无需手动输入时间格式。 +- 单次快捷执行:支持设置“几分钟/几小时/几天后执行”,也提供常用快捷按钮。 +- 触发预览:实时预览未来 5 次执行时间,便于检查规则是否符合预期。 +- 消息模板:支持 `{CurrentTime}`、`{TaskName}`、`{TriggerType}` 动态变量。 +- 执行日志:记录请求 JSON、响应内容、状态码、耗时和错误信息。 +- 托盘运行:关闭窗口时默认隐藏到系统托盘,避免打断工作流。 +- 低系统侵入:不提供开机自启功能,不写入 Windows 启动项。 + +## 系统要求 + +- Windows 10/11 x64 +- [.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0)(发布包不内置运行时) +- 本机可访问的 OmniNotify API,默认地址为: + +```text +http://127.0.0.1:19845/notify +``` + +## 下载与运行 + +1. 从仓库 Release 页面下载: + +```text +OmniScheduler-v0.1.0-win-x64.zip +``` + +2. 解压到任意目录。 +3. 双击运行 `OmniScheduler.exe`。 +4. 如系统提示缺少 .NET 运行时,请安装 .NET 8 Desktop Runtime 后再次运行。 + +发布包采用 framework-dependent、多文件形式,不内置运行时,也不启用单文件打包,以减少体积并降低安全软件误报概率。 + +## 快速开始 + +1. 打开 OmniScheduler。 +2. 点击“新建任务”。 +3. 在“常规”页填写任务名称,并确认任务已启用。 +4. 在“触发器”页添加触发器: + - 单次执行:选择具体日期时间,或使用“xx 时间后执行”的快捷设置。 + - 固定间隔:设置每隔多少秒、分钟、小时或天执行。 + - 每日/每周/每月:选择执行时间和对应日期条件。 + - Cron 表达式:输入高级规则并查看未来执行时间预览。 +5. 在“消息动作”页填写频道、标题和内容。 +6. 点击“发送测试消息”确认 OmniNotify 能正常接收。 +7. 保存任务。 + +## 消息格式 + +触发任务时,OmniScheduler 会向配置的 OmniNotify API 地址发送 JSON: + +```json +{ + "channel": "default", + "title": "提醒标题", + "body": "提醒内容" +} +``` + +频道名需要与 OmniNotify 中已创建的频道一致,否则 OmniNotify 可能拒绝请求并返回 `IllegalChannel`。 + +## 动态变量 + +可以在频道、标题和内容中使用以下变量: + +| 变量 | 含义 | +| --- | --- | +| `{CurrentTime}` | 当前触发时间,格式为 `yyyy-MM-dd HH:mm:ss` | +| `{TaskName}` | 当前任务名称 | +| `{TriggerType}` | 当前触发器摘要或触发来源 | + +示例: + +```text +标题:{TaskName} 已触发 +内容:触发时间:{CurrentTime},触发方式:{TriggerType} +``` + +## 触发器说明 + +### 单次执行 + +在指定日期和时间执行一次。适合一次性提醒或临时任务。 + +单次执行支持快捷设置,例如: + +- 5 分钟后 +- 30 分钟后 +- 1 小时后 +- 明天此时 +- 自定义 N 分钟/小时/天后执行 + +### 固定间隔 + +按固定间隔重复执行,例如每 10 分钟、每 2 小时或每 1 天执行一次。可以额外设置起始生效时间和结束失效时间。 + +### 每日、每周、每月 + +适合日历类周期任务: + +- 每日:每天固定时间执行。 +- 每周:选择星期几,并在固定时间执行。 +- 每月:选择每月某日,或每月最后一个工作日执行。 + +### Cron 表达式 + +适合高级用户配置更复杂的时间规则。编辑时可以查看未来 5 次执行时间,用于校验表达式是否符合预期。 + +## 错过触发补偿策略 + +当系统休眠、关机或程序未运行导致错过计划时间时,可以选择: + +- 忽略,等待下一次:不补发错过的任务。 +- 立即补偿一次:恢复运行后补发一次,然后继续后续计划。 + +## 日志与数据存储 + +任务、设置和日志保存在当前用户目录: + +```text +%LOCALAPPDATA%\OmniScheduler\state.json +``` + +你可以在全局设置中调整: + +- OmniNotify API 地址 +- 日志保留天数 +- 最大日志条数 + +## 常见问题 + +### 为什么 release 包里没有 .NET 运行时? + +为了减小体积,并降低杀毒软件对自包含单文件程序的误报概率。请在系统中安装 .NET 8 Desktop Runtime。 + +### 为什么没有开机自启? + +为了降低对用户系统的影响,OmniScheduler 不会写入 Windows 启动项。如果需要开机运行,建议由用户自行通过系统任务计划程序或快捷方式管理。 + +### 关闭窗口后程序为什么还在运行? + +OmniScheduler 是调度工具,关闭主窗口时默认隐藏到系统托盘。需要完全退出时,请在托盘菜单中选择“退出”。 + +### 发送失败怎么办? + +请优先检查: + +- OmniNotify 是否正在运行。 +- OmniNotify API 地址是否正确。 +- 频道名是否存在并拼写一致。 +- 执行日志中的响应内容和错误信息。 + +## 许可证 + +当前仓库暂未声明许可证。使用、分发或二次开发前,请先与维护者确认授权方式。