docs: 精简架构文档并更新仓库规则

This commit is contained in:
ysicing committed 2026-09-24 12:38:38 +08:00
1 parent b70a621e93
commit 24757efeba
2 files changed
+39 -95

No files matched your search

+26 -22
View File
@@ -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.
```
+13 -73
View File
@@ -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/<owner>/xstats-server:<sanitized-branch>-<full-commit-sha>`. Pull requests do not run this workflow.
## WebDAV settings sync
Account login, OAuth callbacks and the old account backend have been removed.