Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

TrayTidy 是面向 Windows 11 x64 的本地后台应用整理工具。它常驻系统托盘;左键点击图标后,会在任务栏右下角打开一个简洁窗口,直接列出当前用户应用,并标注“正在活动”“建议清理”“可考虑清理”或“建议保留”。

系统组件默认隐藏且不会成为第一层清理对象。程序完全离线,不读取用户内容,也不会自动结束任何应用。
系统组件默认隐藏且不会成为第一层清理对象。核心扫描与清理默认完全本地;可选 AI 应用说明功能默认关闭,仅在用户主动配置并启用后访问指定模型接口。程序不读取用户内容,也不会自动结束任何应用。

## 界面预览

Expand Down Expand Up @@ -42,13 +42,23 @@ TrayTidy 是面向 Windows 11 x64 的本地后台应用整理工具。它常驻

TrayTidy **不扫描音频**,不注册前台使用跟踪 Hook,不持续采样资源,也不保存长期使用轨迹。

## 可选 AI 应用说明

主界面的“AI 说明”是一个独立显示开关:默认收起以保持紧凑行高,打开时按当前列表刷新应用用途和条件式文字建议,关闭时取消当前请求并折叠全部说明,但不会删除有效缓存。“AI 说明”可连接用户指定的 OpenAI-compatible Chat Completions 接口,默认不访问网络,也不会改变本地推荐评分、推荐等级、默认勾选、用户勾选、保护规则或关闭行为;`RecommendationEngine` 和关闭前安全复核始终是这些行为的唯一依据。扫描会先立即显示本地结果,AI 缓存与网络请求在独立支路异步执行。

localhost、127.0.0.1 和 `[::1]` 接口可以使用 HTTP 或 HTTPS;远程接口必须使用 HTTPS,并由用户明确勾选允许远程访问。API Key 可留空以支持本地无认证接口;非空凭据只保存到 Windows Credential Manager 的 `TrayTidy/AiAdvisorApiKey`,不会写入设置、SQLite、日志、URL、缓存键或请求正文。

会发送给模型的数据仅包括:应用显示名、exe 文件名、去重后的进程文件名列表、产品/文件/厂商描述、包系列名称、进程数量、内存/CPU/I/O 档位、窗口和活动状态、本地推荐等级及历史关闭/保留次数汇总。

不会发送:AppKey、完整路径、PID、精确启动时间、Windows 用户名、窗口标题、命令行、环境变量、文件内容、聊天/文档/浏览器页面、API Key、Token 或原始日志。模型输出按不可信外部输入处理,并在展示前再次经过本地安全降级。

## 隐私与安全

- 不采集键盘、鼠标、剪贴板、命令行、完整窗口标题、聊天、文档、浏览器页面、网络请求、密钥或文件内容。
- 不联网,不含遥测、更新检查或崩溃上传。
- 默认不联网,不含遥测、更新检查或崩溃上传;仅在用户启用 AI 说明后访问用户指定的模型接口。
- 系统关键进程、Session 0、其他用户会话和 Windows 组件受硬保护并从主列表隐藏。
- 关闭前重新验证 PID 与进程启动时间,避免 PID 复用导致误操作。
- 执行前重新检查前台窗口、疑似模态对话框,并短时复核 CPU/I/O 活动;状态改变时取消操作。
- 执行前重新检查前台窗口和疑似模态对话框;CPU/I/O 活动只影响推荐和默认勾选,不阻断用户明确发起的正常关闭请求。
- 优先发送正常退出请求;只有进程仍存在且用户再次确认“彻底终止”时,才会强制结束进程树。
- 窗口弹出、扫描完成和界面内操作提示均不播放提示音。
- 日常运行保持 `asInvoker` 且不使用 `SeDebugPrivilege`;仅当用户明确选择彻底终止且权限不足时,发起一次 Windows UAC 请求。提权请求会校验内容完整性。
Expand Down Expand Up @@ -82,6 +92,7 @@ TrayTidy **不扫描音频**,不注册前台使用跟踪 Hook,不持续采
- `src/TrayTidy.App`:WPF 界面、托盘和应用生命周期。
- `src/TrayTidy.Core`:应用分组、推荐规则及安全契约。
- `src/TrayTidy.Infrastructure.Windows`:Windows 进程、窗口、退出和启动项实现。
- `src/TrayTidy.Infrastructure.AI`:可选 AI 说明的脱敏、端点校验、HTTP 协议、响应解析、安全降级和缓存指纹。
- `src/TrayTidy.Persistence`:本地 SQLite 设置与决策记录。
- `tests/`:核心逻辑、持久化和 Windows 安全边界测试。
- `docs/`:架构、安全边界与手工测试说明。
Expand All @@ -96,7 +107,7 @@ TrayTidy **不扫描音频**,不注册前台使用跟踪 Hook,不持续采

## 本地数据

规则、设置和关闭结果位于 `%LocalAppData%\TrayTidy\traytidy.db`,日志位于 `%LocalAppData%\TrayTidy\logs\`。日志最多保留 7 天,关闭结果最多保留 30 天,均不记录用户内容。旧版本遗留的数据库表不会被读取或继续写入。
规则、设置、关闭结果和经过验证的 AI 说明缓存位于 `%LocalAppData%\TrayTidy\traytidy.db`,日志位于 `%LocalAppData%\TrayTidy\logs\`。模型凭据不进入 SQLite。日志最多保留 7 天,关闭结果最多保留 30 天,均不记录用户内容、完整模型请求或响应。旧版本遗留的数据库表不会被读取或继续写入。

## 已知限制

Expand Down
30 changes: 30 additions & 0 deletions TrayTidy.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrayTidy.Core", "src\TrayTi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrayTidy.Infrastructure.Windows", "src\TrayTidy.Infrastructure.Windows\TrayTidy.Infrastructure.Windows.csproj", "{A28A3079-8CAE-4B0E-88F9-EABF4FAF4E0E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrayTidy.Infrastructure.AI", "src\TrayTidy.Infrastructure.AI\TrayTidy.Infrastructure.AI.csproj", "{D88A94C0-C02C-4B3B-88C1-0715D314B770}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrayTidy.Persistence", "src\TrayTidy.Persistence\TrayTidy.Persistence.csproj", "{0C004578-7F99-4C82-A631-C851566BDEC9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
Expand All @@ -21,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrayTidy.Persistence.Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrayTidy.Windows.IntegrationTests", "tests\TrayTidy.Windows.IntegrationTests\TrayTidy.Windows.IntegrationTests.csproj", "{462D9F2E-39BE-4931-9249-11ADEE9EACDA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrayTidy.Infrastructure.AI.Tests", "tests\TrayTidy.Infrastructure.AI.Tests\TrayTidy.Infrastructure.AI.Tests.csproj", "{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -67,6 +71,18 @@ Global
{A28A3079-8CAE-4B0E-88F9-EABF4FAF4E0E}.Release|x64.Build.0 = Release|Any CPU
{A28A3079-8CAE-4B0E-88F9-EABF4FAF4E0E}.Release|x86.ActiveCfg = Release|Any CPU
{A28A3079-8CAE-4B0E-88F9-EABF4FAF4E0E}.Release|x86.Build.0 = Release|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Debug|x64.ActiveCfg = Debug|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Debug|x64.Build.0 = Debug|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Debug|x86.ActiveCfg = Debug|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Debug|x86.Build.0 = Debug|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Release|Any CPU.Build.0 = Release|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Release|x64.ActiveCfg = Release|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Release|x64.Build.0 = Release|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Release|x86.ActiveCfg = Release|Any CPU
{D88A94C0-C02C-4B3B-88C1-0715D314B770}.Release|x86.Build.0 = Release|Any CPU
{0C004578-7F99-4C82-A631-C851566BDEC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C004578-7F99-4C82-A631-C851566BDEC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C004578-7F99-4C82-A631-C851566BDEC9}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -115,6 +131,18 @@ Global
{462D9F2E-39BE-4931-9249-11ADEE9EACDA}.Release|x64.Build.0 = Release|Any CPU
{462D9F2E-39BE-4931-9249-11ADEE9EACDA}.Release|x86.ActiveCfg = Release|Any CPU
{462D9F2E-39BE-4931-9249-11ADEE9EACDA}.Release|x86.Build.0 = Release|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Debug|x64.ActiveCfg = Debug|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Debug|x64.Build.0 = Debug|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Debug|x86.ActiveCfg = Debug|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Debug|x86.Build.0 = Debug|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Release|Any CPU.Build.0 = Release|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Release|x64.ActiveCfg = Release|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Release|x64.Build.0 = Release|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Release|x86.ActiveCfg = Release|Any CPU
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -123,9 +151,11 @@ Global
{E4A87D1E-4849-4FE5-8FA6-12E57C09BE43} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{9F32AA0E-832D-48A9-96DF-0F67D2A3E70B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A28A3079-8CAE-4B0E-88F9-EABF4FAF4E0E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{D88A94C0-C02C-4B3B-88C1-0715D314B770} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{0C004578-7F99-4C82-A631-C851566BDEC9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{0E709AA8-628A-4941-A681-A6DC2B58CDF3} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{B420E96A-27EB-481E-A087-B313BF691829} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{462D9F2E-39BE-4931-9249-11ADEE9EACDA} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{F8DAE6EF-A3BA-42D0-BC29-FAD7A2AB39F2} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
3 changes: 3 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ try {
dotnet restore TrayTidy.sln
if ($LASTEXITCODE -ne 0) { throw "dotnet restore failed with exit code $LASTEXITCODE." }

dotnet format TrayTidy.sln --verify-no-changes --no-restore --verbosity minimal
if ($LASTEXITCODE -ne 0) { throw "dotnet format failed with exit code $LASTEXITCODE." }

dotnet build TrayTidy.sln -c Release --no-restore
if ($LASTEXITCODE -ne 0) { throw "dotnet build failed with exit code $LASTEXITCODE." }

Expand Down
15 changes: 12 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# 架构说明

TrayTidy 采用四个产品项目,界面与 Windows 平台代码通过 Core 接口隔离:
TrayTidy 采用五个产品项目,界面、AI 与 Windows 平台代码通过 Core 接口隔离:

- `TrayTidy.App`:WPF 紧凑应用列表、保护列表 Tab、托盘、MVVM 命令与生命周期。
- `TrayTidy.Core`:应用分组、推荐、行内提醒、规则和安全契约。
- `TrayTidy.Infrastructure.Windows`:进程/窗口快照、短时 CPU 与 I/O 采样、应用激活、正常退出、系统保护、单实例与启动项。
- `TrayTidy.Infrastructure.AI`:OpenAI-compatible HTTP 客户端、脱敏描述符、端点校验、结构化响应解析、本地安全降级和缓存指纹。
- `TrayTidy.Persistence`:SQLite 设置、规则、决策与关闭结果。

## 扫描流程
Expand All @@ -15,14 +16,22 @@ TrayTidy 采用四个产品项目,界面与 Windows 平台代码通过 Core
4. 对每个应用生成状态、简短理由及必要的行内提醒。
5. 系统保护组只计入“已隐藏”数量,不进入可操作列表。

不存在周期扫描、音频枚举、前台 Hook、长期使用会话或网络调用。
不存在周期扫描、音频枚举、前台 Hook 或长期使用会话。确定性扫描流程不包含 AI 或其他网络调用。

## AI 旁路

AI 说明不进入 `OnDemandScanService`、`RecommendationEngine`、`ApplicationTerminationService` 或提权终止链路。Core 中的 `AiApplicationDescriptor` 是唯一允许发送的应用元数据结构,不含 AppKey、路径、PID、用户名、窗口标题、命令行或用户内容。`TrayTidy.Infrastructure.AI` 只序列化该描述符;Windows API 凭据实现留在 `TrayTidy.Infrastructure.Windows`。

本地扫描完成后,`MainViewModel` 立即替换应用列表。主界面的“AI 说明”采用会话级显示开关并默认关闭;打开时协调器按当前列表读取匹配的 SQLite 缓存,并将未命中缓存的条目一次提交,验证模型完整返回后再按请求 ID 分发到各应用行。关闭开关会取消当前请求并折叠每个应用的 AI 区域,但不删除有效缓存。只有开关保持打开时,扫描后的自动分析设置才会生效。每次扫描递增 generation;进度回调只有在 generation 仍匹配时才更新应用行。新扫描、清理、退出或新分析会取消旧任务,同一时间最多存在一个 AI 分析任务。

Provider fingerprint 仅由规范化 endpoint、model 和 prompt version 计算;缓存键另包含脱敏身份、活动档位和本地推荐等级,不包含 API Key。模型或 endpoint 改变、prompt version 升级、活动档位变化或缓存过期都会阻止旧结果复用。SQLite 只保存验证和本地安全降级后的 insight,不保存原始请求或原始响应。

## 生命周期

应用使用命名 Mutex 和命名管道保证单实例;自动化测试使用独立随机名称,不与正在运行的正式实例冲突。正常启动会显示右下角窗口;关闭窗口只隐藏,托盘右键“退出”才释放托盘图标、扫描取消源、单实例资源和服务容器。`--background` 仅用于用户明确开启的登录启动,不会主动扫描。扫描或清理忙碌时,托盘“重新扫描”不会启动并发操作。

## 关闭契约

主界面先执行正常关闭:执行前重新快照并验证 AppKey、PID、启动时间、系统保护和前台/模态状态,再进行约 1 秒 CPU/I/O 复核,之后才向目标顶层窗口发送 `WM_CLOSE` 并等待。进度通过主窗口内的非模态动画条显示。只有返回“仍在运行”时,界面才提供可忽略的“彻底终止进程”按钮;用户点击后再次执行同样的身份、安全和活动复核,再终止进程树。不会静默升级。普通权限不足时,提权请求文件的 SHA-256 摘要随启动参数传递,管理员进程仅处理内容完整且带有显式强杀授权的请求。
主界面先执行正常关闭:执行前重新快照并验证 AppKey、PID、启动时间、系统保护和前台/模态状态,之后向目标顶层窗口发送 `WM_CLOSE` 并等待。CPU/I/O 和子进程活动只影响推荐与默认勾选;用户明确点击关闭后不会被后台常驻活动拦截,应用仍可自行拒绝退出或显示保存确认。进度通过主窗口内的非模态动画条显示。只有返回“仍在运行”时,界面才提供可忽略的“彻底终止进程”按钮;用户点击后再次执行身份、系统保护和前台/模态复核,再终止进程树。不会静默升级。普通权限不足时,提权请求文件的 SHA-256 摘要随启动参数传递,管理员进程仅处理内容完整且带有显式强杀授权的请求。

只有确认成功退出后才会写入最近一次关闭决策,并对下一次安全扫描提供一次性推荐提升;失败、状态变化或用户忽略不会伪装成成功关闭。如果用户在后续清理中未勾选该推荐项,则记录 `Ignore`,使一次性提升失效。明确的长期关闭/保留统计仍可作为较弱的历史信号,关闭结果在启动时清理至最近 30 天。
16 changes: 15 additions & 1 deletion docs/MANUAL_TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
2. 确认不弹出模态确认框;主界面轻微缩小并变暗,中央出现蓝紫能量环、已选应用数量和扫描光带,处理完成后显示勾选反馈并平滑回到列表。
3. 对会缩小到自身托盘的应用测试,确认窗口内出现可忽略的“彻底终止”按钮。
4. 点击“忽略”,确认进程保留;再次测试并点击“彻底终止”,确认在身份复核后终止进程树并记录结果。
5. 在确认前将目标切到前台、启动 CPU/I/O 任务或打开保存对话框,确认操作被复核并取消。
5. 在确认前将目标切到前台或打开保存对话框,确认操作被复核并取消。
6. 对后台持续产生 CPU/I/O 或包含 CefSharp/Chromium 活动子进程的普通用户应用明确点击“关闭”,确认仍会发送正常退出请求;该活动仍不得使应用被默认勾选。
6. 确认系统组件不会出现在可勾选列表中。
7. 清理过程中点击托盘“重新扫描”,确认不会刷新列表或与当前操作并发。

Expand All @@ -47,3 +48,16 @@
2. 用媒体应用播放音频,确认 TrayTidy 不枚举音频设备或音频会话;推荐仅依据进程/窗口和短时资源活动。
3. 检查本地数据库,确认不再创建或写入前台使用记录。
4. 开启登录启动,检查 HKCU Run 值包含带引号的完整路径和 `--background`;关闭设置后确认值被删除。

## 可选 AI 应用说明

1. 保持 AI 关闭,扫描、筛选和关闭应用;用网络监视工具确认没有模型请求,原有行为不变。
2. 配置 localhost OpenAI-compatible Chat Completions 接口(允许 HTTP),不填写 API Key,确认“测试连接”和手动“AI 说明”可用。
3. 配置远程 HTTPS 接口,确认未勾选远程确认时不能启用;勾选后才能保存并请求。确认远程 HTTP、含 user-info 的 URL 和无效协议均被本地拒绝。
4. 保存、替换和删除 API Key;重新打开设置窗口时只显示“已保存凭据”,不会回填明文。检查 Settings JSON、SQLite 和日志均无凭据。
5. 检查设置窗口的“会发送/不会发送”清单;抓取测试服务收到的请求,确认没有完整路径、PID、用户名、窗口标题、命令行、文件内容、AppKey 或 API Key 正文。
6. 让测试服务返回非法 JSON、未知枚举、重复 ID、超长文本和超大响应,确认应用不崩溃,只提示“AI 分析未完成,本地推荐不受影响”。
7. 让测试服务超时;分析期间重新扫描,确认旧请求取消且旧 generation 结果不显示。退出应用时确认请求也被取消。
8. 记录分析前的复选框、本地状态、分数和保护规则;分析成功或失败后逐项确认完全不变。对前台、模态窗口、活动子任务、AlwaysKeep 和身份不可靠应用确认不会显示关闭建议。
9. 确认主界面“AI 说明”默认是关闭状态且应用行保持紧凑;打开后立即刷新并展开说明,关闭后当前请求取消、所有说明折叠且行高恢复。重新打开时确认有效缓存命中且不重复请求网络;改变 model 或 endpoint 后确认旧缓存不复用。
10. 点击“清除 AI 缓存”,确认说明消失且下次需要重新生成;重启后确认模型接口启用设置仍保留,但主界面 AI 说明开关恢复关闭,API Key 仍只存在 Credential Manager。
Loading