From a3a6a7c91d7e05efbdf8e2f11e57ddf45fcda92f Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Sat, 19 Sep 2026 04:02:51 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFchore:=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=20Windows-Gotify-Client-Rust=20=E9=A1=B9=E7=9B=AE=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 Rust + Slint 重建一个轻量、现代、Windows 原生体验良好的 Gotify 桌面客户端。本次只搭目录骨架与占位文件,未进入 Phase 1(Bootstrap)。 本次提交内容 ------------ - 保留 AGENTS.md(1363 行定制 Agent 实施计划,含 8 阶段路线图) - 新建 Cargo.toml([package] 元数据 + AGENTS.md §44 依赖模板 + §42 release profile;无 [[bin]],等 Phase 1 才加 src/main.rs) - 新建 README.md(项目导览,引用 AGENTS.md) - 新建 .gitignore(Rust / Slint / Windows / IDE / 密钥) - 新建 LICENSE(MIT 占位) - 按 AGENTS.md §4 建好全部 10 个目录,每个空目录放 .gitkeep: assets/ assets/fonts/ ui/ scripts/ src/ src/gotify/ src/storage/ src/windows/ src/services/ tests/ 未提交(Phase 1 起才落地) ------------------------ - src/main.rs 及所有 .rs 业务代码 - ui/*.slint 视图 - scripts/build-release.ps1 等构建脚本 - Cargo.lock(当前 Cargo.toml 是占位、无可编译代码) 技术栈约束(来自 AGENTS.md §1 / §2) ------------------------------------ - Rust stable + Slint 1.17.x + Windows 10/11 - 显式禁用:Tauri / Wails / Electron / WebView2 / WPF / WinUI 3 / Node 运行时 - 单一主程序,无浏览器运行时依赖;UI 由 Slint 原生渲染 仓库 ---- 远程:https://gitea.kennethcheng.cc/KennethCheng/Windows-Gotify-Client-Rust --- .gitignore | 53 + AGENTS.md | 2236 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 53 + LICENSE | 21 + README.md | 66 ++ assets/.gitkeep | 0 assets/fonts/.gitkeep | 0 scripts/.gitkeep | 0 src/.gitkeep | 0 src/gotify/.gitkeep | 0 src/services/.gitkeep | 0 src/storage/.gitkeep | 0 src/windows/.gitkeep | 0 tests/.gitkeep | 0 ui/.gitkeep | 0 15 files changed, 2429 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/.gitkeep create mode 100644 assets/fonts/.gitkeep create mode 100644 scripts/.gitkeep create mode 100644 src/.gitkeep create mode 100644 src/gotify/.gitkeep create mode 100644 src/services/.gitkeep create mode 100644 src/storage/.gitkeep create mode 100644 src/windows/.gitkeep create mode 100644 tests/.gitkeep create mode 100644 ui/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48a363a --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# --- Rust / Cargo --- +/target/ +**/*.rs.bk +Cargo.lock.bak + +# Keep Cargo.lock tracked once a real build exists in Phase 1. +# Until then, an empty Cargo.toml exists and no lockfile is produced. + +# --- Slint build artifacts --- +ui/*.slint.bak +*.slint~ + +# --- Windows / IDE / OS --- +.vs/ +.vscode/ +.idea/ +*.user +*.suo +*.sdf +*.opensdf +*.cachefile +*.VC.db +*.VC.opendb + +# OS junk +Thumbs.db +ehthumbs.db +ehthumbs_vista.db +.DS_Store +Desktop.ini +$RECYCLE.BIN/ + +# --- Local config / state --- +# The runtime config + state files live under %APPDATA%/WindowsGotify/ +# and are NEVER inside the repo. The list below is just a defensive net. +*.local.json +*.local.toml +config.local.* +state.local.* + +# --- Logs --- +*.log +logs/ + +# --- Secrets --- +.env +.env.* +*.pem +*.key + +# --- UPX / packing artifacts --- +*.upx +packed/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6bfd3ec --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,2236 @@ +# Windows-Gotify-Client-Rust — Agent Implementation Plan + +> 目标:使用 **Rust + Slint** 重建一个轻量、现代、Windows 原生体验良好的 Gotify 桌面客户端。 +> +> 目标不是把现有 C# 项目逐行翻译成 Rust,而是以现有项目的功能为产品基线,重新设计更轻、更清晰、更容易长期维护的架构。 + +--- + +## 0. Agent 必须先读懂的上下文 + +现有项目:`Windows-Gotify-Client` + +当前实现已经包含这些核心能力: + +- Gotify HTTP API +- WebSocket 实时消息 +- WebSocket 断线重连 +- 新消息 Windows 通知 +- 应用/Token 管理 +- 配置保存 +- 最近消息状态保存 +- Windows 托盘 +- 深色/浅色主题 +- 消息列表与详情 +- 点击通知 URL +- 开机启动相关能力 + +本次 Rust 版本应保留成熟的核心功能,但不复制当前代码的类结构。 + +### 产品定位 + +这是一个 **Windows-only、后台常驻、托盘型 Gotify 客户端**。 + +核心原则: + +1. 小体积 +2. 低内存占用 +3. 后台低 CPU +4. 无浏览器运行时依赖 +5. 单一主程序 +6. UI 精致现代 +7. 网络层与 UI 完全解耦 +8. Windows 能力原生调用 +9. 配置/状态简单可靠 +10. 默认不做过度工程化 + +--- + +# 1. 技术栈 + +## 必选 + +- Rust stable +- Slint 1.17.x 或当前稳定兼容版本 +- Windows 10 x64 +- Windows 11 x64 / ARM64(条件允许时支持) +- Cargo + +Slint 当前官方文档将 Windows 10 x86-64 与 Windows 11 x86-64 / ARM64 列为测试平台,并支持 Windows 子系统以隐藏控制台窗口。具体版本以开发时锁定的稳定版为准。 + +## Windows 原生能力 + +优先使用 `windows-rs` 的 focused crates;只有 focused crate 不覆盖目标 API 时,才使用 `windows` / `windows-sys`。 + +候选: + +- `windows-notifyicon`:托盘图标 +- `windows-registry`:开机启动等 Registry 操作 +- `windows-window`:只有确实需要时使用 +- `windows`:Toast/WinRT/其它必要 Win32/COM API + +不要为了方便把整个 Windows API 集合无脑全部引入。 + +## 网络 + +推荐: + +- `reqwest`:HTTP +- `tungstenite`:WebSocket +- `serde` +- `serde_json` +- `url` + +默认优先使用 blocking HTTP + 独立网络线程,而不是为了一个 WebSocket 客户端引入完整 async runtime。 + +只有当实现明确证明 async 能降低复杂度或改善功能时,才引入 `tokio`。 + +## 日志 + +推荐: + +- `tracing` +- `tracing-subscriber` + +Release 默认降低日志等级,不允许每条消息都产生大量磁盘 IO。 + +## 本地数据 + +V1 默认使用 JSON 文件: + +```text +config.json +state.json +``` + +不要一开始引入 SQLite。 + +只有产品明确需要: + +- 大规模历史记录 +- 搜索历史 +- 消息标签 +- 长期存档 + +才考虑 SQLite。 + +--- + +# 2. 明确禁止的技术路线 + +除非出现无法解决的技术限制,否则禁止: + +- Tauri +- Wails +- Electron +- WebView2 作为 UI 主体 +- Web 前端 + WebView +- Windows App SDK +- WPF +- WinUI 3 +- 大型前端框架 +- Node.js runtime +- 浏览器 runtime + +原因不是这些技术不好,而是本项目目标是一个 **极轻量 Windows 常驻工具**。 + +UI 应由 Slint 原生渲染,业务逻辑由 Rust 完成。 + +--- + +# 3. 核心架构 + +采用: + +```text + ┌──────────────────────────┐ + │ Slint UI │ + │ │ + │ Dashboard / Message │ + │ Detail / Settings │ + └────────────┬─────────────┘ + │ + UI callbacks + │ + ▼ + ┌──────────────────────────┐ + │ AppCore │ + │ │ + │ Config │ + │ Connection state │ + │ Message state │ + │ User actions │ + └───────┬─────────┬────────┘ + │ │ + AppCommand │ │ AppEvent + │ │ + ▼ ▼ + ┌──────────────────────────┐ + │ Network Worker │ + │ │ + │ REST API │ + │ WebSocket │ + │ reconnect / backoff │ + └────────────┬─────────────┘ + │ + ▼ + Gotify Server + + ┌──────────────────── AppCore ────────────────────┐ + │ │ │ + ▼ ▼ ▼ + Storage Worker Windows Host Logger + │ │ + │ ┌─────┼─────┐ + │ ▼ ▼ ▼ + │ Tray Toast Startup + ▼ + config.json + state.json +``` + +### 关键设计 + +UI 永远不能直接: + +- 调用 HTTP +- 管理 WebSocket +- 读取配置文件 +- 修改 Registry +- 调用 Toast +- 操作托盘 + +UI 只通过 `AppCore` 触发操作和接收状态。 + +--- + +# 4. 目录结构 + +目标结构: + +```text +Windows-Gotify-Client-Rust/ +├── Cargo.toml +├── Cargo.lock +├── README.md +├── LICENSE +├── AGENTS.md +├── .gitignore +│ +├── assets/ +│ ├── icon.ico +│ ├── tray.ico +│ ├── app-icon.png +│ └── fonts/ # 可选;尽量不内置字体 +│ +├── ui/ +│ ├── app.slint +│ ├── theme.slint +│ ├── colors.slint +│ ├── components.slint +│ ├── message-card.slint +│ ├── message-list.slint +│ ├── empty-state.slint +│ ├── settings-page.slint +│ ├── server-card.slint +│ └── dialogs.slint +│ +├── src/ +│ ├── main.rs +│ ├── app.rs +│ ├── events.rs +│ ├── commands.rs +│ ├── models.rs +│ ├── error.rs +│ │ +│ ├── gotify/ +│ │ ├── mod.rs +│ │ ├── api.rs +│ │ ├── websocket.rs +│ │ ├── reconnect.rs +│ │ └── model.rs +│ │ +│ ├── storage/ +│ │ ├── mod.rs +│ │ ├── config.rs +│ │ └── state.rs +│ │ +│ ├── windows/ +│ │ ├── mod.rs +│ │ ├── tray.rs +│ │ ├── notification.rs +│ │ ├── startup.rs +│ │ ├── single_instance.rs +│ │ └── url.rs +│ │ +│ └── services/ +│ ├── mod.rs +│ └── lifecycle.rs +│ +├── tests/ +│ ├── gotify_api.rs +│ ├── reconnect.rs +│ ├── state.rs +│ └── parsing.rs +│ +└── scripts/ + ├── build-release.ps1 + └── package.ps1 +``` + +--- + +# 5. UI 产品设计 + +## 总体视觉方向 + +关键词: + +> **Fluent / 极简 / 安静 / 高级 / 信息密度适中 / 长时间使用舒服** + +不要做成: + +- 老式 Windows 管理软件 +- 浏览器 Admin Dashboard +- 过多渐变 +- 大面积阴影 +- 过大的圆角 +- 花哨动画 + +整体感觉应该接近: + +- Windows 11 Fluent +- Linear +- Raycast +- Arc +- 现代桌面通知工具 + +但不要直接复制任何现有产品。 + +--- + +# 6. 主窗口布局 + +推荐默认尺寸: + +```text +960 × 680 px +``` + +最小尺寸: + +```text +760 × 520 px +``` + +布局: + +```text +┌─────────────────────────────────────────────────────────────┐ +│ Gotify ● Connected ─ □ × │ +├──────────────┬──────────────────────────────────────────────┤ +│ │ │ +│ ◉ All │ Messages │ +│ │ ───────────────────────────────────────── │ +│ Servers │ │ +│ ├ Server A │ ┌────────────────────────────────────────┐ │ +│ └ Server B │ │ ● Build succeeded │ │ +│ │ │ CI / Production │ │ +│ │ │ 2 min ago │ │ +│ │ └────────────────────────────────────────┘ │ +│ │ │ +│ Filters │ ┌────────────────────────────────────────┐ │ +│ ○ High │ │ ● Backup finished │ │ +│ ○ Normal │ │ NAS / Backup │ │ +│ ○ Low │ │ 8 min ago │ │ +│ │ └────────────────────────────────────────┘ │ +│ │ │ +│──────────────│ │ +│ ⚙ Settings │ │ +│ ℹ About │ │ +└──────────────┴──────────────────────────────────────────────┘ +``` + +注意:上图只表达结构,不是要求做成 ASCII 风格。 + +--- + +# 7. UI 组件规范 + +## Window + +- 圆角:轻度,约 10~14 px +- 不要夸张玻璃效果 +- 不要大面积半透明 +- 标题栏简洁 +- 支持 Windows 深浅主题 + +## Sidebar + +宽度:约 220~240 px。 + +内容: + +1. App logo +2. All messages +3. Server 列表 +4. Priority filter +5. Settings +6. About + +Sidebar 应该视觉安静,不抢主内容焦点。 + +## Message Card + +每一条消息显示: + +- priority 状态 +- title +- app name +- message preview +- relative time + +例如: + +```text +● Build succeeded + GitLab Runner · production + 2 min ago +``` + +Priority 使用小面积视觉标记,不允许整张卡片使用高饱和背景。 + +推荐表现: + +```text +Low neutral +Normal subtle accent +High amber/orange accent +Max red accent +``` + +但是所有颜色都必须进入主题系统统一管理。 + +--- + +# 8. 消息详情页 + +点击消息后进入右侧详情视图,或使用可折叠 Detail Panel。 + +结构: + +```text +┌───────────────────────────────────────────────┐ +│ ← Message │ +│ │ +│ Build succeeded │ +│ GitLab Runner · Production │ +│ 2026-09-19 03:42:18 │ +│ │ +│ ┌───────────────────────────────────────────┐ │ +│ │ │ │ +│ │ Deploy completed successfully. │ │ +│ │ │ │ +│ │ Version: v1.4.2 │ │ +│ │ Server: production │ │ +│ │ │ │ +│ └───────────────────────────────────────────┘ │ +│ │ +│ [ Copy ] [ Open Link ] │ +└───────────────────────────────────────────────┘ +``` + +正文: + +- 默认 plain text +- 根据 Gotify `client::display` extras 支持 markdown 时,再提供 Markdown 展示 +- 不要为了 Markdown 引入浏览器 +- V1 可以先纯文本,再扩展 Markdown + +Gotify 官方文档定义了 `client::display` extras,并支持 `text/plain` 与 `text/markdown` 等 content type。实现时以服务器实际 API 行为为准。 + +--- + +# 9. 空状态设计 + +没有消息时不要只显示: + +```text +No messages. +``` + +应提供漂亮的空状态: + +```text + ✦ + + You're all caught up + + New Gotify messages will + appear here automatically. + + [ Test Notification ] +``` + +不使用外部图片资源,优先用 Slint 自绘简单图形/图标。 + +--- + +# 10. 设置页面 + +设置页面必须精简,不做企业软件式的巨型表单。 + +分为: + +### Connection + +- Server URL +- App Token +- Connection test +- Auto reconnect +- Connection status + +### Notifications + +- Enable desktop notification +- Sound +- Notify on High priority +- Notify on Max priority +- Click action + +### Startup + +- Start with Windows +- Start minimized +- Minimize to tray +- Close to tray + +### Appearance + +- System +- Light +- Dark +- Compact message density + +### Advanced + +- Log level +- Clear local state +- Reset settings +- Open config directory + +默认隐藏 Advanced 部分。 + +--- + +# 11. 设置交互要求 + +必须做到: + +```text +修改配置 +↓ +UI 本地状态变化 +↓ +点击 Save 或自动保存 +↓ +AppCore 更新 Network Worker +↓ +必要时重连 +``` + +不要让 UI 直接修改 `config.json`。 + +--- + +# 12. 托盘体验 + +应用关闭主窗口时,默认: + +```text +Window hidden +App process remains alive +Tray remains alive +``` + +托盘菜单: + +```text +Open +New messages: 3 +──────────── +Pause notifications +Settings +──────────── +Restart connection +Quit +``` + +托盘图标必须: + +- 有正常态 +- 可选 disconnected 状态 +- 不能使用模糊的小图标 +- Windows 11 任务栏大小下仍能识别 + +Slint 1.17 已提供 `SystemTrayIcon`,可以优先评估 Slint 原生托盘能力;若 Windows 上需要更完整的原生行为,再使用 `windows-rs` focused crate。 + +--- + +# 13. 通知设计 + +新消息到来: + +```text +Gotify +Build succeeded +GitLab Runner · Production +``` + +要求: + +- 标题来自 Gotify message title +- 正文来自 message message +- priority 用于通知层级 +- click.url 存在时支持点击打开 + +Gotify 官方 extras 机制中,`client::*` 为客户端保留命名空间,`client::notification` 用于通知行为定制;实现应该严格按照 Gotify 官方 extras 语义处理,不要自行发明兼容格式。 + +URL 打开逻辑必须: + +1. 只接受显式字符串 URL +2. 解析 URL +3. 至少允许 `https://` / `http://` +4. 拒绝 `file://` +5. 拒绝 `javascript:` +6. 拒绝任意自定义 scheme,除非未来明确增加白名单 + +--- + +# 14. Gotify 网络设计 + +## HTTP + +封装: + +```rust +GotifyApi +``` + +负责: + +- 获取消息 +- 获取应用 +- 创建/读取必要资源 +- 删除消息(如功能需要) +- Server health / version(如功能需要) + +不要让 UI 直接知道 URL 拼接方式。 + +## WebSocket + +封装: + +```rust +GotifyWebSocket +``` + +状态: + +```text +Disconnected +Connecting +Connected +Reconnecting +Stopped +``` + +事件: + +```rust +enum NetworkEvent { + Connected, + Disconnected, + Message(GotifyMessage), + Error(NetworkError), +} +``` + +--- + +# 15. WebSocket 重连 + +必须支持指数退避: + +```text +1s +2s +4s +8s +16s +30s +30s... +``` + +服务器成功连接后重置退避时间。 + +不要: + +```text +loop { + connect(); +} +``` + +禁止高频重连打爆服务器。 + +--- + +# 16. 离线补偿 + +不要简单照搬现有 C# 的 `since=lastId` 逻辑。 + +实现前先验证目标 Gotify Server 版本的分页语义。 + +目标行为: + +```text +程序正常运行 + ↓ +保存最后处理消息 ID + ↓ +程序退出 / 网络断开 + ↓ +重新启动 / 重连 + ↓ +HTTP 补拉缺失消息 + ↓ +去重 + ↓ +更新 UI + ↓ +继续 WebSocket +``` + +必须测试: + +- 断网 10 秒 +- 断网 5 分钟 +- 程序重启 +- Gotify Server 重启 +- 多条消息同时到达 +- 消息 ID 非连续 +- 重复消息 + +--- + +# 17. 消息去重 + +V1 不要建立复杂数据库。 + +采用: + +```rust +VecDeque +HashSet +``` + +维护最近 N 条 ID,例如: + +```text +200 +``` + +规则: + +```text +if seen(id) { + ignore +} +``` + +超过上限后删除最老 ID。 + +如果未来消息历史量扩大,再换 SQLite。 + +--- + +# 18. 状态设计 + +`state.json` 只保存真正需要跨进程持久化的状态。 + +建议: + +```json +{ + "last_message_id": 123456, + "selected_server": "default", + "last_window_width": 960, + "last_window_height": 680, + "version": 1 +} +``` + +不要每收到一条消息就立刻 fsync / rewrite 整个 JSON。 + +采用: + +```text +memory state + ↓ +mark dirty + ↓ +debounce 2~5s + ↓ +safe atomic write +``` + +或者在应用正常退出时保存,并定时 checkpoint。 + +写文件采用: + +```text +state.json.tmp + ↓ +flush + ↓ +atomic replace + ↓ +state.json +``` + +避免异常退出导致 JSON 损坏。 + +--- + +# 19. 配置设计 + +配置文件路径建议: + +```text +%APPDATA%/WindowsGotify/config.json +``` + +不要写入安装目录。 + +配置模型: + +```rust +struct AppConfig { + servers: Vec, + selected_server: Option, + appearance: AppearanceMode, + notifications: NotificationConfig, + startup: StartupConfig, +} +``` + +V1 可以只支持一个 Server,但数据结构直接按多 Server 设计,避免未来再改 schema。 + +--- + +# 20. Token 安全 + +不要把 App Token 打印到日志。 + +禁止: + +```text +Authorization: Bearer xxx +``` + +进入日志。 + +HTTP 错误日志最多打印: + +```text +HTTP 401 Unauthorized +``` + +而不要打印完整请求头。 + +V1 可以继续用明文 JSON 保存 token,但必须: + +- 限制文件访问权限到当前用户 +- 日志中永不泄漏 Token +- UI 中默认密码样式隐藏 + +未来需要再考虑 Windows Credential Manager。 + +--- + +# 21. 多 Server 设计 + +产品层支持: + +```text +Server A +Server B +Server C +``` + +每个 Server: + +```json +{ + "id": "server-a", + "name": "Home", + "url": "https://gotify.example.com", + "token": "...", + "enabled": true +} +``` + +V1 可以只同时连接一个 Server,以减少复杂度。 + +架构上不要把 single server 写死。 + +--- + +# 22. UI 与 Rust 通信 + +Slint 只绑定 Presentation Model。 + +推荐: + +```text +Rust Model + ↓ +Slint properties/models +``` + +不要: + +```text +Slint callback +↓ +HTTP request +↓ +JSON parse +↓ +file IO +``` + +UI callback 应类似: + +```rust +on_connect_clicked +on_message_clicked(id) +on_open_link(url) +on_save_settings() +on_quit() +``` + +然后统一发送给 AppCore。 + +--- + +# 23. AppCore 状态机 + +建议有一个明确的应用状态: + +```rust +enum ConnectionState { + Disabled, + Connecting, + Connected, + Reconnecting { attempt: u32 }, + Error, +} +``` + +UI 根据状态显示: + +```text +Connected → green dot / Connected +Connecting → spinner / Connecting +Reconnecting → amber dot / Reconnecting +Error → red dot / Connection error +``` + +不要在 UI 中自己推断网络状态。 + +--- + +# 24. 错误处理 + +业务错误统一: + +```rust +AppError +``` + +分类: + +```text +ConfigError +NetworkError +WebSocketError +StorageError +WindowsError +ParseError +``` + +用户看到的错误必须是人话,例如: + +```text +Unable to connect to Gotify server. +Check the server URL and token. +``` + +日志中保存技术细节。 + +--- + +# 25. 日志策略 + +Debug: + +```text +详细连接过程 +WebSocket 状态变化 +消息解析 +``` + +Release: + +```text +INFO +WARN +ERROR +``` + +不要默认记录所有消息正文。 + +例如: + +```text +[INFO] Gotify connected +[INFO] Received message id=1024 priority=5 +[WARN] WebSocket disconnected +[INFO] Reconnecting attempt=3 +``` + +而不是: + +```text +[INFO] token=xxx message="这里是一大段隐私内容" +``` + +--- + +# 26. 单实例 + +必须支持: + +```text +双击第二次 +↓ +检测已有实例 +↓ +激活现有窗口 +``` + +不要启动两个 WebSocket 实例。 + +Windows 单实例可以放在: + +```text +src/windows/single_instance.rs +``` + +--- + +# 27. 开机启动 + +设置: + +```text +Start with Windows +Start minimized +``` + +优先使用当前用户 Run 注册表项,不要创建系统服务。 + +示例: + +```text +HKCU\Software\Microsoft\Windows\CurrentVersion\Run +``` + +安装/卸载要做到可重复执行。 + +--- + +# 28. 窗口行为 + +关闭按钮默认行为: + +```text +X +↓ +隐藏窗口 +↓ +继续托盘运行 +``` + +真正退出: + +```text +Tray → Quit +``` + +设置里允许用户选择: + +```text +Close to tray +``` + +未来可支持: + +```text +Minimize to tray +``` + +--- + +# 29. 动画规范 + +允许少量微动画: + +- 页面切换 +- 连接状态变化 +- Toast indicator +- Sidebar hover + +禁止: + +- 长时间 loading 动画 +- 无限循环纯装饰动画 +- 消息列表每条都弹入 +- 高耗能动画 + +动画目标: + +```text +100~180ms +``` + +原则: + +> 让 UI 感觉“活着”,但不要让它“喧闹”。 + +--- + +# 30. 字体 + +优先使用 Windows 系统字体。 + +默认: + +```text +Segoe UI +``` + +不要为了 UI 美观强制带一个大型字体文件。 + +中文显示必须正常。 + +消息正文要支持中文、Emoji 与常见 Unicode 字符。 + +--- + +# 31. 图标 + +优先: + +- 简单 SVG/Path +- Slint 内置或项目内矢量资源 +- Windows Shell 图标 + +避免: + +- 大量 PNG +- 网络下载图标 +- 图标字体 +- 大型 icon package + +--- + +# 32. 轻量化目标 + +Release 目标: + +```text +单 exe +无 Node +无 WebView2 +无 .NET runtime +无 Java +无 Python +``` + +实际 EXE 大小不人为硬性规定,但 Agent 必须记录最终结果。 + +记录: + +```text +Raw EXE size +UPX 后大小(如果使用) +Installed size +Startup memory +Idle memory +Idle CPU +``` + +注意:不要为了体积盲目 UPX;先评估安全软件误报和启动开销。 + +--- + +# 33. 性能基线 + +启动后空闲: + +目标: + +```text +CPU:接近 0% +Memory:尽量 < 50 MB +``` + +说明:这是目标值而不是硬性 ABI 承诺,最终结果必须实测记录。 + +收到大量消息时,不允许: + +- 每条消息触发 UI 全量刷新 +- 每条消息写整个 state.json +- 每条消息产生大量同步文件 IO + +应批量更新模型。 + +--- + +# 34. UI 数据模型 + +消息: + +```rust +struct MessageItem { + id: i64, + app_id: i64, + app_name: String, + title: Option, + message: String, + priority: i32, + timestamp: i64, + click_url: Option, +} +``` + +注意:不要让 Slint 直接持有复杂网络对象。 + +UI 使用简化后的 ViewModel。 + +--- + +# 35. 时间显示 + +消息列表: + +```text +just now +2 min ago +1 hour ago +Yesterday +Sep 18 +``` + +详情显示完整时间: + +```text +2026-09-19 03:42:18 +``` + +时间转换必须统一使用明确的时区策略。 + +--- + +# 36. 搜索与过滤 + +V1 可以支持: + +- title +- message +- app name + +搜索应在 UI model 层处理,而不是每按一个键都请求 Gotify Server。 + +过滤: + +```text +All +Low +Normal +High +Max +``` + +V1 不需要复杂 query language。 + +--- + +# 37. 消息列表性能 + +不要为 1000 条消息一次性创建 1000 个复杂控件。 + +使用 Slint model/view 机制,只维护必要的可视数据。 + +目标: + +```text +100 +500 +1000 +``` + +条消息滚动不卡顿。 + +默认只保留最近 N 条,例如: + +```text +200 +``` + +以后再开放配置。 + +--- + +# 38. 安全边界 + +输入永远视为不可信: + +- Server URL +- App Token +- Message title +- Message body +- click URL +- extras + +特别注意: + +```text +click URL +message extras +markdown +``` + +不能直接把未验证内容当 HTML、脚本或 shell 命令执行。 + +--- + +# 39. Markdown 策略 + +第一阶段: + +```text +text/plain +``` + +第二阶段: + +```text +text/markdown +``` + +禁止为了 Markdown 引入 WebView。 + +优先: + +- 纯 Rust Markdown parser +- Slint 富文本能力 +- 或安全降级为纯文本 + +若 Slint 当前版本无法优雅支持完整 Markdown,V1 直接 plain text 是可接受的。 + +--- + +# 40. 测试 + +必须有自动化测试: + +## Unit + +- JSON config parse +- state serialization +- URL validation +- priority mapping +- deduplication +- reconnect backoff +- time formatting + +## Integration + +- Gotify API mock +- WebSocket mock +- connection loss +- reconnect +- message ordering +- duplicate message + +## Manual Windows + +- Windows 10 x64 +- Windows 11 x64 +- 高 DPI 125% +- 高 DPI 150% +- 中文 Windows 用户环境 +- 系统深色模式 +- 系统浅色模式 +- 开机启动 +- 托盘 +- 第二次启动单实例 +- Toast 点击 +- 断网重连 +- 睡眠/唤醒 + +--- + +# 41. CI/CD + +GitHub/Gitea 均可。 + +至少: + +```text +cargo fmt --check +cargo clippy --all-targets --all-features -- -D warnings +cargo test +cargo build --release +``` + +Windows Release 构建: + +```text +x86_64-pc-windows-msvc +``` + +ARM64: + +```text +aarch64-pc-windows-msvc +``` + +是否最终发布 ARM64,取决于依赖兼容性。 + +--- + +# 42. Release 构建 + +推荐: + +```toml +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true +``` + +但每项设置都需要实际 benchmark。 + +不要仅为了“代码更小”牺牲明显的启动速度。 + +推荐先构建两份: + +```text +release-balanced +release-small +``` + +实测后选默认发布配置。 + +--- + +# 43. Windows 子系统 + +Release 版本不显示控制台窗口。 + +可以采用 Slint 官方推荐的: + +```rust +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +``` + +Debug 保留 console 方便排错。 + +--- + +# 44. Cargo 依赖最小化原则 + +Cargo.toml 目标类似: + +```toml +[dependencies] +slint = "1.17" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.x", default-features = false, features = ["blocking", "json", "rustls-tls"] } +tungstenite = "0.x" +url = "2" +tracing = "0.x" +tracing-subscriber = "0.x" +anyhow = "1" +thiserror = "2" +``` + +Windows-specific dependency只引入确实使用的功能。 + +注意:版本号只是起始模板,Agent 在初始化项目时必须根据当前稳定版本与 Rust toolchain 做兼容验证,并提交 Cargo.lock。 + +--- + +# 45. 为什么 HTTP 默认考虑 rustls + +不要无脑依赖平台 OpenSSL。 + +目标: + +```text +single Windows executable +``` + +使用 rustls 可以避免在 Windows 上额外依赖 OpenSSL runtime 的复杂部署。 + +但必须在 release 构建实际验证证书兼容性和体积。 + +--- + +# 46. 网络线程设计 + +V1 推荐一个 network worker: + +```text +NetworkWorker + ├── HTTP client + └── WebSocket +``` + +消息方向: + +```text +AppCore + ↓ AppCommand +NetworkWorker + +NetworkWorker + ↓ NetworkEvent +AppCore +``` + +Channel: + +```rust +std::sync::mpsc +``` + +或者当前 Rust 项目中更适合的 channel,但避免无意义引入完整 async runtime。 + +--- + +# 47. 生命周期 + +启动: + +```text +main + ↓ +single instance + ↓ +load config + ↓ +load state + ↓ +initialize AppCore + ↓ +start NetworkWorker + ↓ +create Slint UI + ↓ +create tray + ↓ +run UI event loop +``` + +关闭: + +```text +Quit + ↓ +stop accepting UI actions + ↓ +stop NetworkWorker + ↓ +flush state + ↓ +remove temp files + ↓ +destroy tray + ↓ +exit +``` + +--- + +# 48. 首次启动体验 + +没有配置时: + +```text +Welcome to Gotify + +Connect your first Gotify server + +Server URL +[ https://gotify.example.com ] + +Application Token +[ ••••••••••••••• ] + +[ Test Connection ] + + [ Continue ] +``` + +成功: + +```text +✓ Connected + +You're ready to receive notifications. +``` + +不要首次启动就弹出复杂设置窗口。 + +--- + +# 49. 连接测试 + +点击 `Test Connection`: + +```text +Testing... +``` + +成功: + +```text +✓ Server reachable +✓ Token valid +``` + +失败: + +```text +✕ Unable to connect +Check URL / token / TLS / network +``` + +UI 不显示底层 panic 或 stack trace。 + +--- + +# 50. Message Notification 去重 + +如果: + +```text +WebSocket 收到消息 +``` + +已经显示过一次后,不允许因为: + +```text +HTTP catch-up +``` + +再次通知用户。 + +正确流程: + +```text +incoming message + ↓ +Dedup check + ↓ +new? + ┌───┴───┐ + no yes + │ │ +ignore store + │ + ├── UI + └── Notify +``` + +--- + +# 51. 日志与隐私 + +敏感数据: + +```text +token +full Authorization header +private message body(可选) +``` + +默认不记录。 + +配置中加入: + +```text +[ ] Include message body in debug logs +``` + +默认关闭。 + +--- + +# 52. 颜色系统 + +不要在 `.slint` 每个组件里直接写大量颜色值。 + +统一定义: + +```text +Background +Surface +SurfaceElevated +Border +TextPrimary +TextSecondary +TextMuted +Accent +Success +Warning +Danger +``` + +Light: + +```text +明亮、低饱和、中性 +``` + +Dark: + +```text +深灰,而不是纯黑 +``` + +Accent 只用于: + +- selected item +- connection status +- primary button +- focus + +不要整个 UI 变成“蓝色海洋”。 + +--- + +# 53. 圆角与间距 + +建立统一 spacing scale: + +```text +4 +8 +12 +16 +20 +24 +32 +``` + +Radius: + +```text +6 +8 +12 +16 +``` + +遵循统一系统。 + +--- + +# 54. 可访问性 + +最低要求: + +- 键盘可导航 +- Tab 顺序合理 +- Enter 执行主要按钮 +- Escape 关闭对话框 +- 文本对比度可读 +- 不仅依靠颜色表达 priority + +未来如果 Slint/Windows Accessibility 支持允许,再扩展完整无障碍。 + +--- + +# 55. 国际化 + +V1 默认中文 + English 架构可扩展。 + +不要把所有 UI 字符串硬编码到业务 Rust 逻辑里。 + +至少做到: + +```text +UI strings + ↓ +centralized resource +``` + +默认语言: + +```text +简体中文 +``` + +英文作为 fallback。 + +如果实现成本过高,V1 先 English/Chinese 单语言构建也可,但字符串必须集中管理。 + +--- + +# 56. README 要写清楚 + +README 至少包含: + +1. 截图 +2. 特性 +3. 系统要求 +4. 下载 +5. 配置方式 +6. Gotify 兼容性 +7. 开发环境 +8. 编译方式 +9. Release 构建 +10. 隐私说明 +11. License + +不要堆大量理论说明。 + +--- + +# 57. Agent 的实施顺序 + +严格按照以下阶段执行。 + +## Phase 1 — Bootstrap + +完成: + +- Cargo project +- Slint hello window +- release profile +- Windows subsystem +- logging +- error handling + +验收: + +```text +cargo run +cargo build --release +``` + +可以打开无 console 的 Windows GUI。 + +## Phase 2 — UI Shell + +完成: + +- MainWindow +- Sidebar +- Message list +- Empty state +- Settings +- Dark/Light/System +- Window state + +此阶段先使用 mock messages,不接网络。 + +## Phase 3 — Gotify API + +完成: + +- Server config +- HTTP client +- connection test +- fetch applications if needed +- message fetch + +## Phase 4 — WebSocket + +完成: + +- connection state +- realtime message +- reconnect backoff +- graceful shutdown + +## Phase 5 — State + +完成: + +- config.json +- state.json +- atomic save +- debounce checkpoint + +## Phase 6 — Windows Integration + +完成: + +- tray +- toast +- URL open +- startup +- single instance + +## Phase 7 — Polish + +完成: + +- animation +- typography +- icons +- empty states +- error states +- loading states +- high DPI +- keyboard navigation + +## Phase 8 — Performance + +完成: + +- profile release +- measure RAM +- measure CPU +- startup time +- executable size +- message burst test + +## Phase 9 — Release + +完成: + +- installer / portable zip +- checksum +- version +- release notes +- clean machine validation + +--- + +# 58. Agent 工作规则 + +## Rule 1 + +不要一次性写完整应用。 + +必须每阶段可运行、可编译。 + +## Rule 2 + +每次修改后运行: + +```text +cargo fmt +cargo check +cargo clippy +cargo test +``` + +适用时再执行 Windows release build。 + +## Rule 3 + +如果 Slint 某个 UI API 与预期不一致,先查当前版本官方文档,再实现。 + +不要凭记忆发明 API。 + +## Rule 4 + +如果 windows-rs API 不确定,优先查 focused crate 文档和官方示例。 + +## Rule 5 + +不要新增依赖来解决一个可以用标准库解决的小问题。 + +## Rule 6 + +不要为了抽象而抽象。 + +V1 需要的是: + +```text +简单 +稳定 +清晰 +``` + +## Rule 7 + +任何网络线程都不能直接操作 UI。 + +## Rule 8 + +任何 Windows API 调用都尽量封装在 `src/windows/`。 + +## Rule 9 + +Token 绝不能进入普通日志。 + +## Rule 10 + +每一个新增第三方依赖都要回答: + +```text +为什么标准库/Slint/windows-rs 不够? +``` + +--- + +# 59. Commit 规则 + +建议: + +```text +feat(ui): add main window shell +feat(gotify): add REST client +feat(gotify): add websocket worker +feat(windows): add tray integration +feat(windows): add toast notifications +feat(storage): add atomic state persistence +fix(gotify): handle reconnect correctly +perf(ui): batch message model updates +refactor(core): decouple network from UI +``` + +每个 commit 尽可能保持单一职责。 + +--- + +# 60. 最终验收清单 + +## 产品 + +- [ ] 首次启动可完成配置 +- [ ] 可以连接 Gotify +- [ ] WebSocket 正常收消息 +- [ ] 断线自动重连 +- [ ] 重启后可以补消息 +- [ ] 不重复通知 +- [ ] Windows Toast 正常 +- [ ] 点击 URL 正常 +- [ ] 托盘正常 +- [ ] 关闭窗口默认不退出 +- [ ] 托盘 Quit 正常退出 +- [ ] 开机启动可用 +- [ ] 第二次启动不创建第二实例 + +## UI + +- [ ] Light +- [ ] Dark +- [ ] System +- [ ] Empty state +- [ ] Loading state +- [ ] Error state +- [ ] Message detail +- [ ] Settings +- [ ] High DPI +- [ ] 中文 +- [ ] Emoji +- [ ] 键盘操作 + +## 工程 + +- [ ] `cargo fmt` +- [ ] `cargo clippy` +- [ ] `cargo test` +- [ ] `cargo build --release` +- [ ] 无 Node +- [ ] 无 WebView2 +- [ ] 无 .NET runtime +- [ ] 无 Windows App SDK +- [ ] 无 OpenSSL runtime 依赖(如果选 rustls) + +## 性能 + +- [ ] 记录 Release EXE 大小 +- [ ] 记录冷启动时间 +- [ ] 记录 Idle RAM +- [ ] 记录 Idle CPU +- [ ] 记录 1000 条消息测试结果 +- [ ] 记录 reconnect 压测结果 + +--- + +# 61. 推荐的最终产品形态 + +最终应该形成: + +```text +Windows-Gotify-Client-Rust + + ┌─────────────────────────────┐ + │ │ + │ Beautiful UI │ + │ Slint │ + │ │ + └──────────────┬──────────────┘ + │ + AppCore + │ + ┌───────────┼───────────┐ + │ │ │ + Gotify Storage Windows + │ │ │ + REST + WS JSON Tray/Toast + │ Startup + │ Mutex + ▼ + Gotify Server +``` + +产品气质: + +> 打开时精致,后台时安静。 + +不要追求“功能最多”,而是让它成为一个 **安装后几乎不用管,但消息来了马上可靠提醒** 的 Windows 小工具。 + +--- + +# 62. 第一版完成定义(Definition of Done) + +当以下条件全部满足时,V1 才算完成: + +1. 全部核心网络逻辑与 UI 解耦。 +2. Slint 负责桌面 UI。 +3. Rust 负责业务与网络。 +4. Windows 原生能力集中在 Windows adapter 层。 +5. WebSocket 有稳定自动重连。 +6. 离线补偿经过实际 Gotify Server 验证。 +7. 消息去重可靠。 +8. Tray / Toast / Startup / Single Instance 可用。 +9. 配置与状态写盘具备原子性。 +10. Release 为单独的 Windows 原生应用,不依赖 WebView2、.NET、Node 或 Windows App SDK。 +11. Light / Dark / System 三套主题视觉完整。 +12. UI 在 1000 条消息规模下保持流畅。 +13. 完成 Windows 10/11 实机验证。 +14. README 与发布包完整。 + +--- + +# 63. 开始编码时的第一条任务 + +Agent 不要立即实现 Gotify。 + +第一步只做: + +```text +Phase 1 ++ +Phase 2 UI Shell ++ +Mock Message Data +``` + +先把下面这个界面做到成品级: + +```text +┌─────────────────────────────────────────────────────────────┐ +│ Gotify ● Connected │ +├──────────────┬──────────────────────────────────────────────┤ +│ All Messages │ Messages │ +│ │ │ +│ Servers │ Build succeeded │ +│ Home │ GitLab Runner · Production │ +│ NAS │ 2 min ago │ +│ │ │ +│ Filters │ Backup finished │ +│ Normal │ NAS · Backup │ +│ High │ 8 min ago │ +│ Max │ │ +│ │ │ +│ Settings │ │ +└──────────────┴──────────────────────────────────────────────┘ +``` + +要求: + +- 真实可操作 +- 深色/浅色 +- hover 状态 +- selected 状态 +- 空状态 +- loading 状态 +- error 状态 +- 高 DPI 正常 +- 中文正常 + +完成 UI Shell 后再进入网络层。 + +--- + +# 64. 最终设计判断 + +优先级顺序固定为: + +```text +可靠性 + > +简洁性 + > +资源占用 + > +可维护性 + > +动画与装饰 +``` + +任何新功能如果会显著增加: + +- runtime +- binary size +- memory +- dependency count +- Windows deployment complexity + +必须重新评估是否值得加入。 + +这个项目的核心竞争力不是功能数量,而是: + +> **轻、快、稳、漂亮。** + +--- + +## 官方参考 + +- Slint Desktop / Fluent / Native UI:https://slint.dev/desktop +- Slint Windows platform:https://docs.slint.dev/latest/docs/slint/guide/platforms/desktop/windows/general/ +- Slint Rust API:https://docs.slint.dev/latest/docs/rust/slint/ +- Slint SystemTrayIcon:https://docs.slint.dev/latest/docs/slint/reference/window/systemtrayicon/ +- Microsoft windows-rs:https://github.com/microsoft/windows-rs +- windows-rs crate selection:https://github.com/microsoft/windows-rs/blob/master/docs/crates/windows.md +- Gotify Message Extras:https://gotify.net/docs/msgextras + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c78a5fa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,53 @@ +# Placeholder Cargo.toml — actual configuration is finalized in Phase 1 (Bootstrap). +# See AGENTS.md §44 (dependency rationale) and §42 (release profile) for guidance. +# +# Until Phase 1 lands: +# - No [[bin]] / src/main.rs is declared yet, so `cargo build` is a no-op. +# - The dependency list below is a copy of the AGENTS.md template; +# versions will be verified against the locked Rust toolchain in Phase 1. +# - No Cargo.lock will be committed until a real build exists. + +[package] +name = "windows-gotify-client-rust" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +description = "Lightweight, modern, Windows-native Gotify desktop client (Rust + Slint)" +license = "MIT" +readme = "README.md" +authors = ["KennethCheng "] + +[dependencies] +# --- UI --- +slint = "1.17" + +# --- Serialization --- +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# --- Networking (blocking HTTP + plain WebSocket, no full async runtime yet) --- +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } +tungstenite = "0.21" +url = "2" + +# --- Logging --- +tracing = "0.1" +tracing-subscriber = "0.3" + +# --- Error handling --- +anyhow = "1" +thiserror = "2" + +# Windows-only focused crates are added on demand in Phase 6 (Windows Integration). +# Do not import the umbrella `windows` crate wholesale — see AGENTS.md §1. + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true + +[profile.release-balanced] +inherits = "release" +opt-level = 3 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53c0ade --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 KennethCheng + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d32b1f --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Windows-Gotify-Client-Rust + +轻量、现代、Windows 原生体验良好的 Gotify 桌面客户端,使用 **Rust + Slint** 构建。 + +> 详细的产品定位、技术栈约束、架构、UI 规范与分阶段实施计划全部记录在 [`AGENTS.md`](./AGENTS.md)。本文档只做项目导览。 + +## 状态 + +🚧 **从 0 起步** —— 当前仓库只有目录骨架与占位 `Cargo.toml`,尚未进入 Phase 1(Bootstrap)。 +完整 8 阶段路线图见 `AGENTS.md` §57。 + +## 目标平台 + +- Windows 10 x64 +- Windows 11 x64(ARM64 条件允许时支持) +- 后台常驻、托盘型客户端 + +## 技术栈 + +| 层 | 选型 | +|---|---| +| 语言 | Rust stable | +| UI | Slint 1.17.x | +| HTTP | `reqwest`(blocking + rustls) | +| WebSocket | `tungstenite` | +| Windows 集成 | `windows-rs` focused crates(按需引入) | +| 配置 / 状态 | JSON 文件(`%APPDATA%/WindowsGotify/`) | +| 日志 | `tracing` + `tracing-subscriber` | + +显式禁用:Tauri / Wails / Electron / WebView2 / WPF / WinUI 3 / Node 运行时。 +理由见 `AGENTS.md` §2。 + +## 仓库布局 + +``` +Windows-Gotify-Client-Rust/ +├── Cargo.toml # 占位 — 见 AGENTS.md §44 +├── AGENTS.md # AI Agent 实施计划(1363 行) +├── README.md +├── LICENSE # MIT(占位) +├── .gitignore +├── assets/ # 图标、字体等 +├── ui/ # Slint 视图 +├── src/ # Rust 业务代码 +│ ├── gotify/ # HTTP + WebSocket +│ ├── storage/ # config.json / state.json +│ ├── windows/ # 托盘 / Toast / 启动 / 单实例 / URL +│ └── services/ # 生命周期 +├── tests/ # 集成测试 +└── scripts/ # PowerShell 构建脚本 +``` + +## 开发环境 + +> 本仓库目前还没有源码,本节将在 Phase 1 完成后补全具体的 `cargo run` / `cargo build --release` 步骤。 + +## 隐私与安全 + +- App Token 默认以 JSON 明文存储在 `%APPDATA%/WindowsGotify/config.json`,仅当前用户可访问 +- 日志永不打印完整 `Authorization` 头 +- 通知点击的 `click.url` 仅允许 `http://` / `https://`,拒绝 `file://` / `javascript:` / 自定义 scheme +- 详细策略见 `AGENTS.md` §13 / §20 / §38 / §51 + +## License + +MIT(见 [LICENSE](./LICENSE))。 \ No newline at end of file diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/fonts/.gitkeep b/assets/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/.gitkeep b/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/gotify/.gitkeep b/src/gotify/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/services/.gitkeep b/src/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/storage/.gitkeep b/src/storage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/windows/.gitkeep b/src/windows/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ui/.gitkeep b/ui/.gitkeep new file mode 100644 index 0000000..e69de29