Release v0.2.0
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ obj/
|
||||
.vs/
|
||||
*.user
|
||||
*.suo
|
||||
artifacts/
|
||||
|
||||
@@ -184,7 +184,6 @@
|
||||
<TabItem Header="全局设置">
|
||||
<Border Background="White" BorderBrush="#D8DEE8" BorderThickness="1" CornerRadius="6" Padding="18" Margin="0,14,0,0">
|
||||
<StackPanel Width="520" HorizontalAlignment="Left">
|
||||
<CheckBox x:Name="StartWithWindowsBox" Content="开机自启" Margin="0,0,0,14" />
|
||||
<CheckBox x:Name="DndBox" Content="免打扰模式:接收但不弹窗" Margin="0,0,0,14" />
|
||||
<UniformGrid Columns="2">
|
||||
<TextBlock Text="每秒最多处理消息数" VerticalAlignment="Center" />
|
||||
|
||||
@@ -247,7 +247,6 @@ public partial class MainWindow : Window
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
StartWithWindowsBox.IsChecked = _state.Settings.StartWithWindows;
|
||||
DndBox.IsChecked = _state.Settings.DndEnabled;
|
||||
RateLimitBox.Text = _state.Settings.MaxMessagesPerSecond.ToString();
|
||||
RetainDaysBox.Text = _state.Settings.RetainDays.ToString();
|
||||
@@ -257,13 +256,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
_state.Settings.StartWithWindows = StartWithWindowsBox.IsChecked == true;
|
||||
_state.Settings.DndEnabled = DndBox.IsChecked == true;
|
||||
_state.Settings.MaxMessagesPerSecond = ReadInt(RateLimitBox, _state.Settings.MaxMessagesPerSecond);
|
||||
_state.Settings.RetainDays = ReadInt(RetainDaysBox, _state.Settings.RetainDays);
|
||||
_state.Settings.RetainCount = ReadInt(RetainCountBox, _state.Settings.RetainCount);
|
||||
_state.Settings.LocalPort = ReadInt(PortBox, _state.Settings.LocalPort);
|
||||
StartupManager.Apply(_state.Settings.StartWithWindows);
|
||||
}
|
||||
|
||||
private void RefreshHistoryFilters()
|
||||
|
||||
@@ -54,7 +54,6 @@ public sealed class AppState
|
||||
|
||||
public sealed class GlobalSettings
|
||||
{
|
||||
public bool StartWithWindows { get; set; }
|
||||
public bool DndEnabled { get; set; }
|
||||
public bool CircuitBreakerOpen { get; set; }
|
||||
public int MaxMessagesPerSecond { get; set; } = 8;
|
||||
|
||||
@@ -8,6 +8,25 @@
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AssemblyName>OmniNotify</AssemblyName>
|
||||
<RootNamespace>OmniNotify</RootNamespace>
|
||||
<Version>0.2.0</Version>
|
||||
<FileVersion>0.2.0.0</FileVersion>
|
||||
<AssemblyVersion>0.2.0.0</AssemblyVersion>
|
||||
<Authors>OmniNotify</Authors>
|
||||
<Company>OmniNotify</Company>
|
||||
<Product>Omni-Notify</Product>
|
||||
<Description>Local desktop notification popup receiver.</Description>
|
||||
<Copyright>Copyright (c) OmniNotify</Copyright>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<NeutralLanguage>zh-CN</NeutralLanguage>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
67
PRD.md
67
PRD.md
@@ -1,67 +0,0 @@
|
||||
# 📦 Omni-Notify 产品需求与交互说明书 (PRD)
|
||||
|
||||
## 一、 产品定位与全局原则
|
||||
* **产品名称:** Omni-Notify
|
||||
* **核心功能:** 接收本机其他应用的格式化信息,进行高度自定义的视觉弹窗展示。
|
||||
* **适用系统:** windows10以上系统。
|
||||
* **全局核心原则:**
|
||||
1. **绝对零交互:** 弹窗纯视觉展示,绝对不抢夺焦点,不可点击,不可交互。
|
||||
2. **扁平化频道制:** 弹窗样式与“频道(Channel)”一对一强绑定。
|
||||
3. **性能优先:** 具备完善的日志清理与全局熔断保护机制。
|
||||
|
||||
---
|
||||
|
||||
## 二、 核心模块拆解与业务逻辑
|
||||
|
||||
### 模块 1:全局设置与托盘 (Tray & Global Settings)
|
||||
* **系统托盘入口:**
|
||||
* 左键:呼出主控面板。
|
||||
* 右键菜单:打开主控面板、全局设置、免打扰(DND)开关、**解除熔断(动态显示,仅熔断时高亮)**、退出软件。
|
||||
* **全局设置面板:**
|
||||
* **基础设置:** 开机自启。
|
||||
* **历史清理策略:** 设定阈值(如保留最近 X 天,或最多保留 X 条),超限自动静默清理。
|
||||
* **免打扰模式 (DND):** 开启后,所有接收到的消息不触发弹窗,但正常静默写入“历史面板”。
|
||||
* **防刷屏熔断机制(Rate Limit):**
|
||||
* **设定:** 全局阈值(如“每秒最多处理 X 条信息”)。
|
||||
* **触发表现:** 超限瞬间,系统直接抛弃后续所有新消息(不弹窗、不进历史面板)。
|
||||
* **熔断通知:** 触发瞬间,系统生成一条不受熔断限制的“系统级弹窗”(固定文案:检测到大量垃圾消息,已开启熔断保护)。
|
||||
* **解除方式:** 必须由用户手动在“托盘右键菜单”或“全局设置面板”点击【解除熔断】方可恢复。
|
||||
|
||||
### 模块 2:频道管理 (Channel Management)
|
||||
* **信息接收匹配逻辑:**
|
||||
* 采用**“主动创建,严格匹配”**模式。
|
||||
* 用户需在主控面板新建频道,自定义“频道名”(唯一标识符)。其他应用发来的信息必须包含该频道名。
|
||||
* **异常处理(严格模式):** 收到未注册/拼写错误的频道信息,直接丢弃不弹窗,并在历史面板记录一条报错日志(标记为:非法来源/未匹配频道)。
|
||||
* **主控面板列表:** 展示所有已建频道,提供新建、删除、重命名、编辑弹窗样式等基础管理功能。
|
||||
|
||||
### 模块 3:弹窗样式设计器 (Profile Designer) - 核心
|
||||
本模块为频道提供独立的样式配置表单。无需“所见即所得”预览,依赖外部发送测试信息验证。
|
||||
|
||||
**3.1 弹窗内容构成**
|
||||
* 仅支持纯文本,严格划分为:**标题(Title)** 与 **正文(Body)** 两个区块。不支持图标等非文本元素。
|
||||
|
||||
**3.2 UI 配置参数分组**
|
||||
1. **多屏与位置:** 指定显示器(主屏/副屏1/副屏2...)、屏幕九宫格位置(左上、上中、右下等)、屏幕边距(位置在边中央时为一个值,在角落时为两个值,在屏幕正中央时无需值)。
|
||||
2. **尺寸与排版:** 宽度、最大高度。
|
||||
3. **视觉样式:** 内边距、字体类型、字号(标题/正文可分设)、文字颜色、背景颜色与透明度、边框样式/颜色/透明度、整体透明度。
|
||||
4. **时间与动画:** 存在时间(X秒)、出现动画(淡入/滑入/放大等)、消失动画。
|
||||
|
||||
**3.3 高级行为规则:多消息堆叠模式(3选1)**
|
||||
1. **队列排队:** 同一位置只显 1 条。新消息入队,上一条消失后,下一条按原动画出现。
|
||||
2. **推挤平移:** 新消息出现将老消息推挤开。**核心细节:**如果新消息先消失,老消息需重新滑回原位置。**边界规则:**无视屏幕物理边界,超限的老消息在屏幕外继续维持其存在时间与坐标逻辑,不强制销毁。
|
||||
3. **直接覆盖:** 新消息直接替换当前弹窗的标题与正文,并重新开始倒计时。
|
||||
|
||||
**3.4 高级行为规则:溢出处理模式(3选1)**
|
||||
*前提:当正文内容过多,达到设定的“最大高度”时触发。*
|
||||
1. **截断:** 超出最大高度的内容隐藏,末尾显示“...”。
|
||||
2. **跑马灯:** 文本在弹窗内匀速滚动。**核心细节(时间动态补偿):**
|
||||
* 若设定存在时间 < 滚动显示完所需时间:滚动完毕后,停留 X 秒(用户设定)再消失。
|
||||
* 若设定存在时间 > 滚动显示完所需时间:等待原有存在时间耗尽后再消失。
|
||||
3. **分割信息:** 严格按“最大高度所能容纳的最大行数”为标准切断。切割后的多段信息,以一定间隔时间(用户设定)依次作为独立弹窗展示。
|
||||
|
||||
### 模块 4:历史信息面板 (History Panel)
|
||||
* **数据展示:** 列表形态,字段包含:接收时间、频道来源、状态(成功展示 / 免打扰静默 / 非法拦截)。
|
||||
* **查询检索:** 支持按时间段、按频道来源、按状态进行高级筛选;支持关键字搜索。
|
||||
* **交互操作(右键菜单):**
|
||||
1. **复制完整内容:** 将该条信息的 Title 和 Body 复制到剪贴板。
|
||||
2. **重新显示该弹窗(复播):** 纯视觉回放,按照该频道当前的样式重新走一遍弹窗流程,**不**在历史面板生成新的接收记录。
|
||||
17
Properties/PublishProfiles/FolderProfile.pubxml
Normal file
17
Properties/PublishProfiles/FolderProfile.pubxml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net8.0-windows\win-x64\publish\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<DebugType>embedded</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
164
README.md
164
README.md
@@ -1,36 +1,154 @@
|
||||
# Omni-Notify
|
||||
# Omni-Notify 用户文档
|
||||
|
||||
Omni-Notify is a Windows 10+ WPF desktop app for receiving local structured messages and showing configurable, non-interactive visual popups.
|
||||
Omni-Notify 是一款 Windows 本地通知弹窗工具。它接收来自本机脚本、自动化任务或其他程序的结构化消息,并以可配置的频道样式显示轻量弹窗。
|
||||
|
||||
## Tech Stack
|
||||
它适合这些场景:
|
||||
|
||||
- .NET 8 WPF for the desktop UI and popup windows
|
||||
- Windows Forms `NotifyIcon` for the system tray entry
|
||||
- Built-in `HttpListener` for local message intake
|
||||
- JSON persistence under `%LOCALAPPDATA%\OmniNotify\state.json`
|
||||
- 构建、部署、备份等脚本完成后弹出提示。
|
||||
- 本地监控任务发现异常时发出视觉提醒。
|
||||
- 工作流工具向桌面发送不抢焦点的状态通知。
|
||||
- 不想使用系统通知中心,希望弹窗样式、位置和历史记录可控。
|
||||
|
||||
## Local Message API
|
||||
## 安装要求
|
||||
|
||||
When the app is running, it listens by default on:
|
||||
- Windows 10 或更高版本
|
||||
- .NET 8 Desktop Runtime
|
||||
|
||||
Omni-Notify 的发布包不内置 .NET 运行时,因此体积较小。首次运行前,请确认电脑已安装 .NET 8 Desktop Runtime。
|
||||
|
||||
## 下载与运行
|
||||
|
||||
下载发布包:
|
||||
|
||||
```text
|
||||
omni-notify-v0.2.0-win-x64.zip
|
||||
```
|
||||
|
||||
解压后运行:
|
||||
|
||||
```text
|
||||
OmniNotify.exe
|
||||
```
|
||||
|
||||
应用启动后会出现在系统托盘。左键点击托盘图标可显示或隐藏主控制面板,右键点击可打开菜单。
|
||||
|
||||
## 快速发送一条通知
|
||||
|
||||
默认接收地址:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:19845/notify
|
||||
```
|
||||
|
||||
Send a POST request with UTF-8 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "default",
|
||||
"title": "Build finished",
|
||||
"body": "The nightly job completed successfully."
|
||||
}
|
||||
```
|
||||
|
||||
Channels must be created in the control panel first. Unknown channels are blocked and recorded in history as `IllegalChannel`.
|
||||
|
||||
## Build
|
||||
PowerShell 示例:
|
||||
|
||||
```powershell
|
||||
dotnet build
|
||||
Invoke-RestMethod `
|
||||
-Uri "http://127.0.0.1:19845/notify" `
|
||||
-Method Post `
|
||||
-ContentType "application/json; charset=utf-8" `
|
||||
-Body '{"channel":"default","title":"Build finished","body":"The nightly job completed successfully."}'
|
||||
```
|
||||
|
||||
JSON 字段说明:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `channel` | 是 | 频道名称,必须已在主面板中创建 |
|
||||
| `title` | 否 | 弹窗标题 |
|
||||
| `body` | 否 | 弹窗正文 |
|
||||
|
||||
如果发送到不存在的频道,消息不会弹窗,但会在历史记录中标记为无效频道。
|
||||
|
||||
## 频道
|
||||
|
||||
频道用于区分不同来源或不同用途的通知。每个频道都可以独立配置弹窗样式。
|
||||
|
||||
常见用法:
|
||||
|
||||
- `default`:普通通知。
|
||||
- `build`:构建或 CI 任务。
|
||||
- `monitor`:监控告警。
|
||||
- `backup`:备份任务。
|
||||
|
||||
在主控制面板的“频道”页中可以新建、删除、重命名频道,并配置该频道的显示效果。
|
||||
|
||||
## 弹窗样式
|
||||
|
||||
每个频道支持配置:
|
||||
|
||||
- 显示器序号和屏幕位置。
|
||||
- 边距、宽度、最大高度和内边距。
|
||||
- 字体、标题字号、正文字号。
|
||||
- 文本颜色、背景颜色、边框颜色和透明度。
|
||||
- 存在时间、进入动画和退出动画。
|
||||
- 多条消息的堆叠方式。
|
||||
- 正文过长时的截断、滚动或拆分显示方式。
|
||||
|
||||
建议先配置频道,再点击“发送测试”观察实际效果。
|
||||
|
||||
## 历史记录
|
||||
|
||||
历史记录页会记录接收过的消息,包括:
|
||||
|
||||
- 接收时间
|
||||
- 频道
|
||||
- 状态
|
||||
- 标题
|
||||
- 正文
|
||||
|
||||
你可以搜索、筛选、清空历史记录,也可以右键复制消息内容或重新显示某条历史消息。
|
||||
|
||||
## 全局设置
|
||||
|
||||
全局设置包括:
|
||||
|
||||
- 免打扰模式:继续接收消息并记录历史,但不弹窗。
|
||||
- 每秒最多处理消息数:超过阈值后触发熔断保护。
|
||||
- 历史保留天数。
|
||||
- 历史最多保留条数。
|
||||
- 本地监听端口。
|
||||
|
||||
Omni-Notify v0.2.0 已移除开机自启功能。应用不会写入 Windows 启动项注册表。
|
||||
|
||||
## 熔断保护
|
||||
|
||||
如果短时间内收到大量消息,Omni-Notify 会自动进入熔断状态,防止弹窗刷屏或持续写入大量历史记录。
|
||||
|
||||
触发熔断后,你可以在托盘菜单或全局设置页中手动解除。
|
||||
|
||||
## 常见问题
|
||||
|
||||
**发送请求后没有弹窗**
|
||||
|
||||
先确认频道名称是否存在,再确认是否开启了免打扰模式。也可以查看历史记录中的状态。
|
||||
|
||||
**提示无法连接本地地址**
|
||||
|
||||
确认 Omni-Notify 正在运行,并检查全局设置中的本地监听端口。如果修改了端口,请使用新的端口发送请求。
|
||||
|
||||
**为什么发布包这么小**
|
||||
|
||||
发布包采用 framework-dependent 模式,不内置 .NET 运行时。这样体积更小,文件结构也更透明。
|
||||
|
||||
**是否会开机自动启动**
|
||||
|
||||
不会。v0.2.0 已删除开机自启功能,也不会写入 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`。
|
||||
|
||||
**杀毒软件仍然提示风险怎么办**
|
||||
|
||||
建议使用正式签名的发布版本,并避免从不可信来源下载。Omni-Notify 的官方发布方式不使用混淆、壳、自解压或单文件打包。
|
||||
|
||||
## 数据位置
|
||||
|
||||
配置和历史记录保存在:
|
||||
|
||||
```text
|
||||
%LOCALAPPDATA%\OmniNotify\state.json
|
||||
```
|
||||
|
||||
需要重置配置时,可以在退出应用后删除该文件。
|
||||
|
||||
## 面向开发者
|
||||
|
||||
如果你需要构建、修改或发布项目,请阅读 [开发者文档](docs/development.md)。
|
||||
|
||||
27
Services.cs
27
Services.cs
@@ -1,10 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace OmniNotify;
|
||||
|
||||
@@ -61,7 +59,6 @@ public sealed class AppStore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class NotificationRouter
|
||||
{
|
||||
private readonly AppState _state;
|
||||
@@ -169,7 +166,6 @@ public sealed class NotificationRouter
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LocalHttpServer : IDisposable
|
||||
{
|
||||
private readonly NotificationRouter _router;
|
||||
@@ -273,26 +269,3 @@ public sealed class LocalHttpServer : IDisposable
|
||||
_listener?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public static class StartupManager
|
||||
{
|
||||
private const string AppName = "OmniNotify";
|
||||
|
||||
public static void Apply(bool enabled)
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true);
|
||||
if (key is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
key.SetValue(AppName, Process.GetCurrentProcess().MainModule?.FileName ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
key.DeleteValue(AppName, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
app.manifest
Normal file
16
app.manifest
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="0.2.0.0" name="OmniNotify.app" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
189
docs/development.md
Normal file
189
docs/development.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Omni-Notify 开发者文档
|
||||
|
||||
本文档面向需要构建、修改、审查或发布 Omni-Notify 的开发者。最终用户安装和使用说明请见 [README.md](../README.md)。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- .NET 8
|
||||
- WPF
|
||||
- Windows Forms `NotifyIcon`
|
||||
- Built-in `HttpListener`
|
||||
- JSON 本地持久化
|
||||
|
||||
项目不依赖第三方 NuGet 包。
|
||||
|
||||
## 项目结构
|
||||
|
||||
| 文件 | 责任 |
|
||||
| --- | --- |
|
||||
| `App.xaml.cs` | 应用生命周期、单实例互斥、托盘入口、本地 HTTP 服务启动 |
|
||||
| `MainWindow.xaml` | 主控制面板 UI |
|
||||
| `MainWindow.xaml.cs` | 频道管理、历史筛选、全局设置保存 |
|
||||
| `Models.cs` | 应用状态、频道、弹窗样式、历史记录、输入消息模型 |
|
||||
| `Services.cs` | 状态持久化、消息路由、限流熔断、本地 HTTP 接收服务 |
|
||||
| `PopupCoordinator.cs` | 弹窗队列、堆叠、替换和位置计算 |
|
||||
| `PopupWindow.xaml` | 单个弹窗窗口 UI |
|
||||
| `PopupWindow.xaml.cs` | 弹窗动画、生命周期和正文溢出处理 |
|
||||
| `app.manifest` | Windows 应用清单,声明普通用户权限运行 |
|
||||
| `scripts/package-release.ps1` | 标准发布脚本 |
|
||||
|
||||
## 本地构建
|
||||
|
||||
```powershell
|
||||
dotnet build .\OmniNotify.csproj -c Release
|
||||
```
|
||||
|
||||
目标框架:
|
||||
|
||||
```xml
|
||||
net8.0-windows
|
||||
```
|
||||
|
||||
应用类型为 WPF 桌面应用,输出为 `WinExe`。
|
||||
|
||||
## 本地运行
|
||||
|
||||
```powershell
|
||||
dotnet run --project .\OmniNotify.csproj
|
||||
```
|
||||
|
||||
默认监听:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:19845/
|
||||
```
|
||||
|
||||
通知接口:
|
||||
|
||||
```text
|
||||
POST http://127.0.0.1:19845/notify
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "default",
|
||||
"title": "Build finished",
|
||||
"body": "The nightly job completed successfully."
|
||||
}
|
||||
```
|
||||
|
||||
## 状态持久化
|
||||
|
||||
状态文件路径:
|
||||
|
||||
```text
|
||||
%LOCALAPPDATA%\OmniNotify\state.json
|
||||
```
|
||||
|
||||
保存内容包括:
|
||||
|
||||
- 全局设置
|
||||
- 频道列表和频道样式
|
||||
- 历史记录
|
||||
|
||||
`AppStore.Save` 会在写入前清理超出保留天数和数量限制的历史记录。
|
||||
|
||||
## 消息处理流程
|
||||
|
||||
1. `LocalHttpServer` 接收本地 HTTP 请求。
|
||||
2. 请求体反序列化为 `IncomingMessage`。
|
||||
3. `NotificationRouter.Receive` 进入 UI Dispatcher。
|
||||
4. 检查熔断状态和每秒消息上限。
|
||||
5. 按 `channel` 严格匹配频道。
|
||||
6. 根据免打扰状态决定是否弹窗。
|
||||
7. 写入历史记录。
|
||||
8. `PopupCoordinator` 按频道样式显示弹窗。
|
||||
|
||||
未知频道不会弹窗,会记录为 `IllegalChannel`。
|
||||
|
||||
## 发布包
|
||||
|
||||
生成 v0.2.0 Windows x64 发布包:
|
||||
|
||||
```powershell
|
||||
.\scripts\package-release.ps1 -Version 0.2.0
|
||||
```
|
||||
|
||||
默认产物:
|
||||
|
||||
```text
|
||||
artifacts\omni-notify-v0.2.0-win-x64.zip
|
||||
```
|
||||
|
||||
脚本参数:
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `Configuration` | `Release` | 构建配置 |
|
||||
| `Version` | `0.2.0` | 应用版本和产物版本 |
|
||||
| `RuntimeIdentifier` | `win-x64` | 目标运行时标识 |
|
||||
|
||||
## 发布策略
|
||||
|
||||
当前发布策略刻意保持透明、标准和低风险:
|
||||
|
||||
- `SelfContained=false`
|
||||
- `PublishSingleFile=false`
|
||||
- `PublishTrimmed=false`
|
||||
- `PublishReadyToRun=false`
|
||||
- `--self-contained false`
|
||||
- 不使用混淆器
|
||||
- 不使用 UPX
|
||||
- 不使用自解压启动器
|
||||
- 不请求管理员权限
|
||||
|
||||
发布脚本会检查产物目录中是否出现以下 .NET 运行时文件:
|
||||
|
||||
- `coreclr.dll`
|
||||
- `clrjit.dll`
|
||||
- `hostfxr.dll`
|
||||
- `hostpolicy.dll`
|
||||
|
||||
如果出现这些文件,脚本会失败,避免误发布自包含运行时包。
|
||||
|
||||
## 安全敏感行为约束
|
||||
|
||||
v0.2.0 已删除开机自启功能。代码中不应重新引入以下行为,除非经过明确产品决策:
|
||||
|
||||
- 写入 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
|
||||
- 创建计划任务实现自启动
|
||||
- 后台下载或执行外部程序
|
||||
- 全局键盘/鼠标钩子
|
||||
- 注入、提权、隐藏进程或绕过安全产品
|
||||
|
||||
应用保留的本地监听服务仅绑定 `127.0.0.1`,不应改为 `0.0.0.0` 或局域网地址,除非同时补充认证和明确的安全设计。
|
||||
|
||||
## 版本与 Git
|
||||
|
||||
发布新版本时:
|
||||
|
||||
1. 更新 `OmniNotify.csproj` 中的 `Version`、`FileVersion`、`AssemblyVersion`。
|
||||
2. 更新 `app.manifest` 中的 `assemblyIdentity version`。
|
||||
3. 更新文档中的示例版本号。
|
||||
4. 运行发布脚本生成 zip。
|
||||
5. 验证发布目录不包含 .NET 运行时文件。
|
||||
6. 提交代码。
|
||||
7. 创建 tag,例如 `v0.2.0`。
|
||||
8. 推送分支和 tag。
|
||||
|
||||
建议 tag 命名:
|
||||
|
||||
```text
|
||||
vMAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
建议发布资产命名:
|
||||
|
||||
```text
|
||||
omni-notify-vMAJOR.MINOR.PATCH-win-x64.zip
|
||||
```
|
||||
|
||||
## v0.2.0 发布摘要
|
||||
|
||||
- 删除开机自启功能及相关注册表写入逻辑。
|
||||
- 新增标准应用清单,声明普通用户权限运行。
|
||||
- 新增 framework-dependent 发布脚本。
|
||||
- 发布包改为 `omni-notify-v0.2.0-win-x64.zip`。
|
||||
- README 改为用户文档,开发信息迁入本文档。
|
||||
60
scripts/package-release.ps1
Normal file
60
scripts/package-release.ps1
Normal file
@@ -0,0 +1,60 @@
|
||||
param(
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Version = "0.2.0",
|
||||
[string]$RuntimeIdentifier = "win-x64"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$publishProfile = "FolderProfile"
|
||||
$publishDir = Join-Path $repoRoot "bin\$Configuration\net8.0-windows\$RuntimeIdentifier\publish"
|
||||
$artifactsDir = Join-Path $repoRoot "artifacts"
|
||||
$zipPath = Join-Path $artifactsDir "omni-notify-v$Version-$RuntimeIdentifier.zip"
|
||||
|
||||
Set-Location $repoRoot
|
||||
|
||||
function Invoke-DotNet {
|
||||
param([string[]]$Arguments)
|
||||
|
||||
& dotnet @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet $($Arguments -join ' ') failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-DotNet @("restore", ".\OmniNotify.csproj")
|
||||
Invoke-DotNet @("clean", ".\OmniNotify.csproj", "-c", $Configuration)
|
||||
Invoke-DotNet @(
|
||||
"publish",
|
||||
".\OmniNotify.csproj",
|
||||
"-c",
|
||||
$Configuration,
|
||||
"-r",
|
||||
$RuntimeIdentifier,
|
||||
"--self-contained",
|
||||
"false",
|
||||
"-p:PublishProfile=$publishProfile",
|
||||
"-p:Version=$Version"
|
||||
)
|
||||
|
||||
$runtimeFiles = @("coreclr.dll", "clrjit.dll", "hostfxr.dll", "hostpolicy.dll")
|
||||
$bundledRuntime = Get-ChildItem -Path $publishDir -Recurse -File |
|
||||
Where-Object { $runtimeFiles -contains $_.Name }
|
||||
|
||||
if ($bundledRuntime) {
|
||||
$names = ($bundledRuntime | Select-Object -ExpandProperty Name) -join ", "
|
||||
throw "Publish output appears to include .NET runtime files: $names"
|
||||
}
|
||||
|
||||
if (!(Test-Path $artifactsDir)) {
|
||||
New-Item -ItemType Directory -Path $artifactsDir | Out-Null
|
||||
}
|
||||
|
||||
if (Test-Path $zipPath) {
|
||||
Remove-Item $zipPath
|
||||
}
|
||||
|
||||
Compress-Archive -Path (Join-Path $publishDir "*") -DestinationPath $zipPath -CompressionLevel Optimal
|
||||
|
||||
Write-Host "Created $zipPath"
|
||||
Reference in New Issue
Block a user