feat: 实现 Phase 1 — Bootstrap(Slint hello window + 编译验证通过)

完成 AGENTS.md §57 Phase 1 的全部交付:可编译的 Cargo 工程、能打开的
Slint hello window、release profile、Windows subsystem、tracing 日志、
thiserror 错误框架。`cargo build` 与 `cargo build --release` 均通过,
0 warning。

实现概览
--------

新增 / 修改的文件:

  src/lib.rs           lib crate 入口;`slint!{}` 内嵌一个最小 AppWindow
                       (标题 "Gotify",480×320,深灰背景 + 两行文本);
                       `pub fn run() -> AppResult<()>` 调 AppWindow::new
                       + run()。
  src/main.rs          thin binary entry;含
                       `#![cfg_attr(not(debug_assertions),
                          windows_subsystem = "windows")]`
                       让 release build 不弹 console(AGENTS.md §43);
                       debug build 保留 console 让 tracing 日志可见。
  src/error.rs         `AppError` + `AppResult`,thiserror 2.0.20;Phase 1
                       只有 `Logging(String)` 与 `Ui(String)` 两个变体,
                       §24 的 ConfigError / NetworkError / WebSocketError
                       / StorageError / WindowsError / ParseError 等后续
                       Phase 落地时再加。
  src/logging.rs       `init_logging()`:tracing-subscriber 0.3.23,启
                       env-filter feature;debug 默认 debug 级,release
                       默认 info 级,`RUST_LOG` 可覆盖;关闭 ANSI(release
                       通常没有真实终端)。符合 §25 的"默认不记录消息正文"
                       基线——具体红线靠代码 review 强制。
  Cargo.toml           实际可编译的 manifest:[[bin]] 指向 src/main.rs;
                       仅引入 Phase 1 真正用到的依赖(slint / tracing /
                       tracing-subscriber / thiserror);reqwest /
                       tungstenite / serde / url / anyhow 等推迟到对应
                       Phase 的模块创建后再引入——避免提前下载无用 crate,
                       也让 AGENTS.md §44 的 "Cargo 依赖最小化原则" 落到
                       每一阶段的 commit 上。
  Cargo.lock           已生成(564 个 transitive deps,含 slint 1.18.0、
                       skia-bindings、accesskit、parley、fontique 等)。
                       二进制工程,lock 文件进 git——保证可重现构建。

技术选型说明
------------

- Slint 版本选择:AGENTS.md §1 写的是 "Slint 1.17.x 或当前稳定兼容版
  本"。首次尝试钉死 `slint = { version = "1", features =
  ["internal-interpreter"] }`,cargo 报 `internal-interpreter` feature 不
  存在(slint 1.18.0 已移除该 feature,slint!{} macro 走 default feature
  set 即可)。改为 `slint = "1"`,cargo 自动选到 1.18.0,与 Rust 1.98.1 +
  MSVC 工具链编译通过。AGENTS.md §1 的措辞明确允许"当前稳定兼容版本",
  本次选 1.18.0 不偏离约束。
- release profile:opt-level=z / lto / codegen-units=1 / panic=abort /
  strip=true(AGENTS.md §42 模板)。Phase 1 未做 benchmark,所以 release
  vs release-balanced 的实际选择留到 Phase 8 Performance。
- 没引 slint-build / 没建 build.rs:Phase 1 hello window 内嵌在
  `slint!{}` macro 里,足以通过验收。Phase 2 切到独立 .slint 文件 +
  `include_slint!()` 时再决定是否需要 build.rs。

验证结果
--------

  cargo build          exit 0, 5.21s(增量;首次冷编译约 1m40s,要编
                                  i-slint-core / parley / skia 等)
  cargo build --release exit 0, 1m38s(profile 内 LTO 阶段耗时长)
  warnings             0
  debug   .exe         25.58 MB, PE subsystem = 3 (WINDOWS_CUI / console)
  release .exe          7.82 MB, PE subsystem = 2 (WINDOWS_GUI)

AGENTS.md §57 Phase 1 验收标准(`cargo run` + `cargo build --release`
能打开无 console 的 Windows GUI)全部满足。PE subsystem 通过直接读
Optional Header 的 Subsystem 字段验证——而不是盲目信任 cfg_attr。

下一步
------

进 Phase 2 — UI Shell(MainWindow / Sidebar / Message list / Empty
state / Settings / Dark-Light-System 主题)。本阶段仍然只用 mock 消息,
不接网络。

遗留
----

- commit message 之前那条 root commit (`a3a6a7c`) subject 前有个 UTF-8
  BOM (`\ufeff`),是 PowerShell 5.1 `Set-Content -Encoding UTF8` 默认带
  BOM 造成的。本 commit 用 `[System.IO.File]::WriteAllText` 直接写
  UTF-8 without BOM,新 commit 不再带 BOM;老的那个根 commit 的 BOM 只
  是显示瑕疵,不影响任何 git 操作,留着不重写。
- 首次 cargo build 拉了 564 个 crate、走代理拉取成功(crates.io 直连也
  通,HTTPS_PROXY 走 127.0.0.1:7890 是兜底)。
- target/ 1.4 GB 在 .gitignore 内,不会进 git。
- 用户的 .cargo/bin 不在 PowerShell 当前 session 的 PATH 上(用 rustup
  默认安装路径 `C:\Users\Administrator\.cargo\bin`),后续 cargo 命令
  脚本里临时加 PATH 即可,未修改用户全局环境。
This commit is contained in:
2026-09-19 04:15:19 +08:00
parent a3a6a7c91d
commit b30b2f0e28
6 changed files with 6037 additions and 24 deletions
Generated
+5893
View File
File diff suppressed because it is too large Load Diff
+19 -24
View File
@@ -1,11 +1,8 @@
# Placeholder Cargo.toml — actual configuration is finalized in Phase 1 (Bootstrap).
# See AGENTS.md §44 (dependency rationale) and §42 (release profile) for guidance.
# Cargo.toml — Phase 1 (Bootstrap).
#
# 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.
# Dependency rationale lives in AGENTS.md §44. Only the crates actually used
# by Phase 1 are listed; the remaining crates (reqwest / tungstenite / serde /
# url / anyhow / ...) are added in later phases when their modules land.
[package]
name = "windows-gotify-client-rust"
@@ -17,31 +14,29 @@ license = "MIT"
readme = "README.md"
authors = ["KennethCheng <a1478925630@163.com>"]
[[bin]]
name = "windows-gotify-client-rust"
path = "src/main.rs"
[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"
# `slint` crate. The `slint!{}` macro is compiled in via the default feature
# set in 1.x (verified against 1.18.0 — `internal-interpreter` was removed in
# 1.x). The Windows GUI backend (`backend-winit`) is also part of defaults.
# Phase 2 will likely switch from the embedded macro to `include_slint!()`
# + standalone .slint files under `ui/`.
slint = "1"
# --- Logging ---
tracing = "0.1"
tracing-subscriber = "0.3"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# --- 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]
# See AGENTS.md §42. The `release-balanced` profile below is the practical
# default after benchmarking — adjust if startup time regresses.
opt-level = "z"
lto = true
codegen-units = 1
@@ -49,5 +44,5 @@ panic = "abort"
strip = true
[profile.release-balanced]
inherits = "release"
inherits = "release"
opt-level = 3
+27
View File
@@ -0,0 +1,27 @@
//! Top-level application error type.
//!
//! The full taxonomy in AGENTS.md §24 is
//! `ConfigError / NetworkError / WebSocketError / StorageError /
//! WindowsError / ParseError`, plus user-facing variants derived from them.
//! Only the categories reachable in the current phase are wired up here; new
//! variants land in later phases.
use thiserror::Error;
pub type AppResult<T> = std::result::Result<T, AppError>;
/// Application-wide error.
///
/// All variants carry a `String` payload in Phase 1 because the upstream
/// errors we propagate are themselves heterogeneous (tracing init, Slint,
/// later reqwest/tungstenite). When the corresponding Phase adds a dedicated
/// module, the String is replaced with a structured error from that module
/// and the `From` impl is added.
#[derive(Debug, Error)]
pub enum AppError {
#[error("logging initialization failed: {0}")]
Logging(String),
#[error("ui error: {0}")]
Ui(String),
}
+57
View File
@@ -0,0 +1,57 @@
//! Windows Gotify Client (Rust) — core library.
//!
//! Phase 1 only exposes the bare-minimum surface needed to spin up the
//! Slint hello window:
//! - [`error`]: AppError / AppResult
//! - [`logging`]: tracing-subscriber init
//! - [`run`]: entry that builds the Slint UI and runs its event loop
//!
//! Larger subsystems (gotify, storage, windows integration, ...) are added in
//! later phases per AGENTS.md §57.
pub mod error;
pub mod logging;
use slint::ComponentHandle;
slint::slint! {
export component AppWindow inherits Window {
title: "Gotify";
width: 480px;
height: 320px;
background: #1f2024;
VerticalLayout {
padding: 24px;
spacing: 12px;
Text {
text: "Windows Gotify Client (Rust)";
color: #e6e6e6;
font-size: 18px;
}
Text {
text: "Phase 1 — Bootstrap OK";
color: #8a8a8a;
font-size: 12px;
}
}
}
}
/// Build the Slint UI and run its event loop until the window is closed.
///
/// In release builds the Windows subsystem is set to `windows` via the
/// `#[cfg_attr]` on the binary entry point, so no console window is shown.
pub fn run() -> AppResult<()> {
tracing::info!(
"starting windows-gotify-client-rust v{}",
env!("CARGO_PKG_VERSION")
);
let ui = AppWindow::new().map_err(|e| AppError::Ui(e.to_string()))?;
ui.run().map_err(|e| AppError::Ui(e.to_string()))
}
pub use error::{AppError, AppResult};
+29
View File
@@ -0,0 +1,29 @@
//! Logging bootstrap — `tracing` + `tracing-subscriber`.
//!
//! Policy (AGENTS.md §25):
//! - Debug builds default to `debug` level.
//! - Release builds default to `info` level.
//! - Message bodies and tokens MUST NOT be logged (enforced by code review,
//! not by this module).
//! - Override at runtime with the `RUST_LOG` env var.
use crate::error::{AppError, AppResult};
use tracing_subscriber::{fmt, EnvFilter};
/// Initialize the global tracing subscriber.
///
/// Idempotent at the type level — repeated calls fail with `AppError::Logging`
/// (tracing only allows one global subscriber per process). That is intentional:
/// the caller should call exactly once during startup.
pub fn init_logging() -> AppResult<()> {
let default_level = if cfg!(debug_assertions) { "debug" } else { "info" };
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(default_level));
fmt()
.with_env_filter(filter)
.with_target(false)
.with_ansi(false)
.try_init()
.map_err(|e| AppError::Logging(e.to_string()))
}
+12
View File
@@ -0,0 +1,12 @@
// Windows GUI application: release builds hide the console window entirely.
// Debug builds keep the console so logs from `tracing` are visible.
//
// See AGENTS.md §43.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use windows_gotify_client_rust::{logging, AppResult};
fn main() -> AppResult<()> {
logging::init_logging()?;
windows_gotify_client_rust::run()
}