From 24757efebaba8dca5ba546dbbf95ea2162a034ed Mon Sep 17 00:00:00 2001 From: ysicing Date: Thu, 24 Sep 2026 12:38:38 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=B2=BE=E7=AE=80=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=B9=B6=E6=9B=B4=E6=96=B0=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 48 ++++++++++++++------------- ARCHITECTURE.md | 86 ++++++++----------------------------------------- 2 files changed, 39 insertions(+), 95 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f10df8b..7cfa179 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,30 +1,34 @@ # XStats 仓库规则 +## 技术栈(Tech Stack) + +- macOS 客户端使用 Swift 6、AppKit 与 SwiftUI,包含 WidgetKit 扩展;目标为 macOS 14+、Apple Silicon(arm64)。 +- `project.yml` 由 XcodeGen 生成 Xcode 工程;可测试模块位于 Swift Package `Packages/XStatsKit`,构建任务定义在 `Taskfile.yml`。 +- 系统采集使用 IOKit / SMC;特权操作通过 `SMAppService` 注册的 XPC 辅助工具完成。本机历史和 AI 统计使用 SQLite,偏好设置使用 UserDefaults,凭据使用 Keychain。 +- 日历使用 Tyme4Swift;更新检查服务位于 `server/api`,使用 Go、Fiber v3、GORM 和 SQLite。依赖版本以 `Packages/XStatsKit/Package.swift` 和 `server/api/go.mod` 为准,构建与发布步骤见 [DEVELOPMENT.md](DEVELOPMENT.md)。 + +## 架构与代码质量 + +- macOS 客户端优先使用系统原生框架和现有模块;引入新的运行时、跨平台 UI 层、依赖或抽象前,先说明当前需求为何不能由现有机制满足。此约束不排斥已有的 Go 服务和必要的第三方库。 +- 新代码保持直接、可读:优先提前返回、清晰分支和有意义的局部变量,避免深层嵌套与层层转发的控制流。 +- 尽量少写嵌套函数;仅在框架回调要求,或局部闭包明显比新增符号更简单时使用。 +- 不要仅为缩短调用方,把只有一个调用点、又不表达稳定业务概念的逻辑抽成模块级辅助函数。可复用行为、接口或框架回调、导出 API、测试夹具,以及值得直接测试的复杂业务逻辑,可以独立成函数;保留单次使用的辅助函数时,名称应表达持久的领域概念,而不是机械步骤。 + +## 性能与资源 + +- 监控采样沿用现有需求驱动机制;新增后台任务应在功能未启用、界面不可见或系统休眠等不需要工作时停止或降频,避免无必要的常驻轮询、重复采集和主线程阻塞。 +- 处理日志、网络响应与历史数据时控制内存、磁盘 I/O 和刷新频率;长任务支持取消,并按实际风险验证耗电、内存及响应性,不为未出现的性能问题提前增加缓存或并发层。 + +## 国际化(i18n) + +- 新增或修改用户可见文案时以简体中文为源文案,使用 `Packages/XStatsKit/Sources/Localization` 的 `tr(...)` 与现有翻译资源,同步补齐受影响语言的译文;不要只在某个视图中硬编码单一语言。系统名称、日志标识和协议字段不作为界面文案翻译。 +- 日期、数字、单位使用当前应用语言对应的 Locale;检查长译文、动态文本及阿拉伯语从右到左布局,避免靠固定宽度或字符串拼接假定中文语序。相关界面改动至少验证受影响语言和布局。 + ## 代码许可 - 本仓库自行编写的新增代码、测试及对上游代码的修改,采用 **AGPL-3.0-or-later**(GNU Affero General Public License 第 3 版或更新版本)。许可范围以 [LICENSING.md](LICENSING.md) 为准,完整许可见 [LICENSE](LICENSE)。 -- 本次确认的 XStats 版权所有者署名为 `ysicing`。新建源码和测试文件时,使用对应语言的注释语法添加 `Copyright (C) <首次发布年份> ysicing` 和 `SPDX-License-Identifier: AGPL-3.0-or-later`;当前年份为 2026。脚本保留首行 shebang,将声明放在其后。不支持注释的文件在许可说明中记录,不破坏文件格式。 -- 跨年发生实质修改时可扩展版权年份范围,不机械刷新年份。文件头不写应用版本号,不虚构邮箱;如需联系邮箱,使用用户确认的地址集中记录在项目文档。其他贡献者按真实归属署名,不将其贡献自动归给 ysicing。 -- 修改上游或第三方文件时,保留原有版权与许可声明,明确区分 XStats 修改部分的授权;禁止批量覆盖第三方许可证或将上游原始代码声称为 AGPL 独占授权。 +- 新建源码和测试文件在支持注释时标注 `SPDX-License-Identifier: AGPL-3.0-or-later`;脚本保留首行 shebang。不支持注释的文件在许可说明中记录,不破坏文件格式;文件头不写应用版本号或虚构邮箱。 +- 修改上游或第三方文件时,保留原有版权与许可声明,明确区分 XStats 修改部分的授权;其他贡献者按真实归属署名,不机械改写已有年份,也不将上游原始代码声称为 AGPL 独占授权。 - OpenStats 原有 MIT 许可与版权声明保留在 [LICENSES/OpenStats-MIT.txt](LICENSES/OpenStats-MIT.txt),其他第三方声明见 [ThirdPartyNotices.md](ThirdPartyNotices.md)。 - 引入第三方代码、依赖或素材时,核对许可兼容性并保留来源、版权及必要声明;不新增与本项目新增代码许可相冲突的默认 MIT 声明。 - 调整许可相关内容时,同步核对 LICENSE、LICENSING.md、第三方声明及四语言 README。不得声称本次许可变更撤销了历史版本已有的 MIT 授权。 - -Swift 新文件示例: - -```swift -// Copyright (C) 2026 ysicing -// SPDX-License-Identifier: AGPL-3.0-or-later -``` - -含 OpenStats 上游代码及 XStats 修改的文件示例(仍需保留文件原有的其他声明): - -```swift -// Copyright (c) 2026 GiantAccel, LLC -// XStats modifications Copyright (C) 2026 ysicing -// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT -// -// Upstream portions retain their MIT license. -// XStats modifications are licensed under AGPL-3.0-or-later. -// See LICENSE, LICENSING.md and LICENSES/OpenStats-MIT.txt. -``` diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c3d79b9..670364c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,68 +1,9 @@ -# XStats +# XStats architecture macOS 14+ menu-bar system monitor. Swift 6 language mode, AppKit status item and panels -hosting SwiftUI, no third-party dependencies. The Xcode project is generated from -`project.yml` by XcodeGen; everything testable lives in the local Swift package. - -- `App/` — `main.swift` and the asset catalog. -- `Widget/` — the sandboxed WidgetKit extension (“System overview”), embedded in `Contents/PlugIns`. - It samples CPU, memory, disk and battery itself through `Metrics`, so it works without the app. -- `Helper/` — the privileged helper (`XStatsHelper`) and its launchd plist, embedded in - the app bundle for `SMAppService.daemon`. -- `Packages/XStatsKit/Sources/Localization` — `tr(_:)`, language resolution and nine-language catalogs (see below). -- `Packages/XStatsKit/Sources/SMC` — the AppleSMC user client, fan control, temperature - key discovery. -- `Packages/XStatsKit/Sources/Metrics` — one sampler per metric and `MetricsHub`; power and CPU - frequency (`PowerSampler`), disk activity and NVMe SMART (`DiskSamplers`), Bluetooth battery, - the SQLite history store. -- `Packages/XStatsKit/Sources/AIUsage` — local usage models, two read-only log scanners and opt-in quota readers. - Quota readers inspect CLI credentials read-only and never refresh or write them. -- `Packages/XStatsKit/Sources/Cleaner` — cleanup rules, `SafetyGuard`, `CleanEngine`, the app - uninstaller's leftover search and the launchd startup-item list. -- `Packages/XStatsKit/Sources/Updates` — the update manifest and the download → verify → - replace → relaunch steps. -- `Packages/XStatsKit/Sources/HelperShared` — the XPC protocol and maintenance commands - shared by the app and the helper. -- `Packages/XStatsKit/Sources/WebDAVSync` — manual WebDAV GET/PUT and endpoint-specific Keychain passwords. -- `Packages/XStatsKit/Sources/XStatsUI` — design tokens, panel pages, settings, - menu-bar renderer, app controller, snapshot renderer. -- `Packages/XStatsKit/Tests` — metrics, SMC decoding, cleanup safety, updates, UI logic and - localization. -- `server/api` — the Fiber/GORM update API, SQLite installation counters and server-rendered statistics dashboard. - -## Build - -**Xcode 26 or later is required** — CommandLineTools does not ship the SwiftUI macro plugins. - -```bash -brew install go-task xcodegen -task build # Release build, then replace /Applications/XStats.app and relaunch -task test # swift test in the package -``` - -Versions read `1.0.0 (110)`: `MARKETING_VERSION` is the semver from the first release heading in -`CHANGELOG.md`, while `CURRENT_PROJECT_VERSION` is an independent, monotonically increasing build -number. `task build` advances only the build number (`BUMP=0` skips it); `task release` takes the -public version from the changelog and advances the build number once. Every `task build` runs -`scripts/install_local.sh`: it ends the running app (the helper restores fans and sleep when the -connection drops), deletes the old `/Applications/XStats.app`, *moves* the new bundle there so no -copy stays in the build folder, re-registers it with Launch Services, and relaunches. Only one -XStats ever exists on the machine, so Spotlight and the widget gallery never show duplicates. -`INSTALL=0` compiles without installing; `scripts/release.sh` uses it and installs the notarized -build at the end. - -`project.yml` defaults to ad-hoc signing so the project opens anywhere. The Taskfile.yml passes -the first Developer ID Application identity from the keychain (and `--timestamp` for Release) -when there is one. `task release` runs `scripts/release.sh`: build, verify team, timestamp and -hardened runtime on both binaries, notarize and staple the app, build and notarize the DMG, -write the online-update zip and `appcast.json`, and write a Homebrew cask (`auto_updates true`) -whose URLs point at the object storage prefix `https://c.ysicing.net/oss/apps/macOS/XStats`. -The release records the source commit plus version-file hashes in `dist/release-provenance.json`; -publishing accepts only a clean descendant that changed release metadata, so the Git tag cannot -silently include source different from the packaged binaries. `task release-all` runs tests, builds, -commits and pushes only release metadata, then publishes every external target; `task publish` -resumes the idempotent external half without rebuilding. The helper derives its client -requirement from its own signing team at run time, so no team ID is hard-coded. +hosting SwiftUI. The Xcode project is generated from `project.yml` by XcodeGen; +the app's testable components live in the local Swift package. For directory layout, build, test, +and release instructions, see [DEVELOPMENT.md](DEVELOPMENT.md). ## Sampling @@ -72,6 +13,9 @@ samples only that: CPU always; memory and network cheaply; GPU, disk, processes, fans only when a visible surface needs them. Disk is read at most every 30 s, battery every 10 s. The loop pauses on screen sleep, system sleep and session switch. +The sandboxed WidgetKit extension samples CPU, memory, disk, and battery through `Metrics` +independently, so it remains useful when the app is not running. + Things that are easy to get wrong and are handled on purpose: - `host_processor_info` returns kernel-allocated memory; it is `vm_deallocate`d every sample. @@ -107,8 +51,9 @@ verifying its version. Unregister/handshake failures leave privileged calls disa Registered with `SMAppService.daemon`; `RunAtLoad` so that an unclean exit is repaired at boot. The XPC interface is a fixed list of operations — no arbitrary commands — and each -connection gets `setCodeSigningRequirement`. State that must be undone (manual fans, disabled -sleep) is persisted to `/Library/Application Support/XStats/helper-state.plist` and +connection gets `setCodeSigningRequirement`, derived from the helper's own signing team rather +than a hard-coded team ID. State that must be undone (manual fans, disabled sleep) is persisted +to `/Library/Application Support/XStats/helper-state.plist` and reverted when the last client disconnects or at the next start. The helper exits after 30 s without clients. @@ -335,9 +280,7 @@ the quota reader has no access to session-log content. service name and every address; without the helper, a one-off administrator prompt runs the same fixed command. -## Online updates - -### Battery and Bluetooth +## Battery and Bluetooth The battery item reuses `BatterySampler` (IOKit power sources, sampled every 10 s while the item is in the menu bar or its popover / page is open). `BatteryPopover` is both the popover and the main-window page @@ -352,6 +295,8 @@ popover / page or the System page is open, every five minutes when the menu bar low-battery hint or a Mac without a battery), otherwise not at all. `AppModel.bluetoothDemand` derives that from the same visibility state as `demand`. +## Online updates + `UpdateController` posts the current version and a hashed random installation ID at launch and daily. China-region locales prefer `https://x-stats.china.12306.work/api/v1/update/check`; other locales prefer `https://xstats-apps.12306.work/api/v1/update/check`. Failures fall back serially to the other endpoint, @@ -380,11 +325,6 @@ that carries the dmg for manual downloads, and only then submits the generated a `scripts/publish_api.py` to both regional services. The manifest lands last, so an installed app never sees a version whose package is not yet in place. -`server/api/Dockerfile` cross-compiles a CGO-free binary for amd64 and arm64, then runs it as the -distroless `nonroot` user with `/data` as the writable SQLite volume. When a branch push changes -`server/**`, `.github/workflows/server-image.yml` builds both platforms and publishes -`ghcr.io//xstats-server:-`. Pull requests do not run this workflow. - ## WebDAV settings sync Account login, OAuth callbacks and the old account backend have been removed.