Initial commit

This commit is contained in:
leefer
2026-08-02 18:08:19 +08:00
commit 89c48131b2
47 changed files with 4743 additions and 0 deletions
+494
View File
@@ -0,0 +1,494 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from `dotnet new gitignore`
# dotenv files
.env
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Vim temporary swap files
*.swp
# Repository-local SDK and release artifacts
.tools/
artifacts/
# The macOS *.app rule above also matches this WPF project on Windows.
!src/WindowsToolbox.App/
!src/WindowsToolbox.App/*.cs
!src/WindowsToolbox.App/*.xaml
!src/WindowsToolbox.App/*.csproj
+10
View File
@@ -0,0 +1,10 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>
<ApplicationIcon Condition="Exists('$(MSBuildThisFileDirectory)assets\WindowsToolbox.ico')">$(MSBuildThisFileDirectory)assets\WindowsToolbox.ico</ApplicationIcon>
</PropertyGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
</packageSources>
</configuration>
+73
View File
@@ -0,0 +1,73 @@
# Windows 工具箱
面向 Windows 10/11 的中文 WPF 系统工具。主程序以普通权限运行;修改系统设置时才启动独立管理员执行器。所有可恢复功能会在第一次修改前保存基线快照,重复执行不会覆盖原始状态。
## 已实现功能
- Windows 11 经典右键菜单:启用、恢复;修改后自动重启 Explorer,在 Build 26200 上立即生效。
- 快捷方式箭头:隐藏、恢复,并生成本地透明图标资源。
- Windows 更新:暂停 1—35 天、每日自动续期、强力关闭、每日关闭守护、恢复原始状态。
- Windows 安全:检测防篡改保护、关闭可控制的 Defender 防护、可选关闭防火墙、恢复原始设置。
- 自动登录:恢复 `netplwiz` 中可能隐藏的密码复选框并打开 Windows 自带账户控制;工具不读取密码。
- 桌面归档:使用 Known Folder API 识别真实桌面,按 `yyyy-MM-dd` 归档普通文件和普通文件夹;排除快捷方式、系统项及重解析目录,自动处理同名文件/文件夹,支持撤销和每月计划任务。
- KMS:内置微软公开的 Windows 10/11 GVLK,使用用户填写的本地 KMS 地址,连接成功后才安装密钥,支持恢复原 KMS 主机。
- 系统状态、逐步执行结果及最近 100 次操作历史。
- 多尺寸应用图标,同时用于窗口标题栏、任务栏和 EXE 文件。
启动时只运行一个有 5 秒硬超时的可选状态查询进程,主窗口不会等待它才显示;普通功能完成后只刷新注册表基础状态,不再重复等待 Defender 与许可证查询。
## 运行
开发构建输出位于:
```text
src\WindowsToolbox.App\bin\Debug\net8.0-windows\WindowsToolbox.App.exe
```
发布目录生成后运行:
```text
artifacts\WindowsToolbox\WindowsToolbox.App.exe
```
运行环境需要 Windows 10/11 x64 和 .NET 8 Desktop Runtime。涉及系统修改的按钮会出现 UAC 提示。
## 构建
仓库已经配置本地 .NET 8 SDK 位置。执行:
```powershell
.\scripts\build-release.ps1
```
生成便携发布目录:
```text
artifacts\WindowsToolbox\
```
项目没有第三方 NuGet 运行依赖。
## 数据位置
```text
%LOCALAPPDATA%\WindowsToolbox\
├─ ArchiveManifests\ 桌面归档恢复清单
├─ Logs\ 操作历史
├─ Pending\ 临时提权命令及结果
└─ Profiles\ 桌面归档配置
%PROGRAMDATA%\WindowsToolbox\
├─ Assets\ 透明快捷方式图标
└─ Snapshots\ 系统设置基线快照
```
## 重要边界
- “强力关闭更新”会关闭可控制的策略、服务和任务,并每日重新检查;Windows 就地修复或重大升级后仍可能恢复受保护组件。
- 工具不会绕过 Defender 防篡改保护。关闭 Defender 前必须由用户在 Windows 安全中心手动关闭防篡改保护。
- 家庭版不支持 KMS 客户端激活。Windows 客户端的正规 KMS 服务还需要满足微软规定的客户端数量门槛。
- KMS 恢复功能可以恢复主机和端口,但 Windows 不提供旧完整产品密钥,无法自动恢复未知旧密钥。
- 跨磁盘文件移动不是文件系统原子操作;移动失败会保留在恢复清单和操作日志中。
更多内部设计见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) 和 [docs/OPERATIONS.md](docs/OPERATIONS.md)。
+64
View File
@@ -0,0 +1,64 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E54A5670-8E0E-463E-8FBF-430054BF2736}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsToolbox.Core", "src\WindowsToolbox.Core\WindowsToolbox.Core.csproj", "{404532FF-0F8C-4A80-A182-3FEBAB789DC3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsToolbox.Infrastructure", "src\WindowsToolbox.Infrastructure\WindowsToolbox.Infrastructure.csproj", "{32D8C232-E015-4DC5-8DF6-D9B6C70D2BC4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsToolbox.App", "src\WindowsToolbox.App\WindowsToolbox.App.csproj", "{44AEEFE7-C6CA-482D-BDDF-54F5CF570295}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsToolbox.ElevatedHost", "src\WindowsToolbox.ElevatedHost\WindowsToolbox.ElevatedHost.csproj", "{61D6B7B9-FB16-4D3C-B0FA-1EE6B5124E22}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsToolbox.Cli", "src\WindowsToolbox.Cli\WindowsToolbox.Cli.csproj", "{BC9B81D5-0862-4465-9357-738BB0B2E4E0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A98D294C-F010-49CE-968F-A85E2F921692}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsToolbox.Verification", "tests\WindowsToolbox.Verification\WindowsToolbox.Verification.csproj", "{8CCFF617-CDF1-4EFD-812E-AE92CD75F8CA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{404532FF-0F8C-4A80-A182-3FEBAB789DC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{404532FF-0F8C-4A80-A182-3FEBAB789DC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{404532FF-0F8C-4A80-A182-3FEBAB789DC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{404532FF-0F8C-4A80-A182-3FEBAB789DC3}.Release|Any CPU.Build.0 = Release|Any CPU
{32D8C232-E015-4DC5-8DF6-D9B6C70D2BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{32D8C232-E015-4DC5-8DF6-D9B6C70D2BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32D8C232-E015-4DC5-8DF6-D9B6C70D2BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32D8C232-E015-4DC5-8DF6-D9B6C70D2BC4}.Release|Any CPU.Build.0 = Release|Any CPU
{44AEEFE7-C6CA-482D-BDDF-54F5CF570295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{44AEEFE7-C6CA-482D-BDDF-54F5CF570295}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44AEEFE7-C6CA-482D-BDDF-54F5CF570295}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44AEEFE7-C6CA-482D-BDDF-54F5CF570295}.Release|Any CPU.Build.0 = Release|Any CPU
{61D6B7B9-FB16-4D3C-B0FA-1EE6B5124E22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61D6B7B9-FB16-4D3C-B0FA-1EE6B5124E22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61D6B7B9-FB16-4D3C-B0FA-1EE6B5124E22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61D6B7B9-FB16-4D3C-B0FA-1EE6B5124E22}.Release|Any CPU.Build.0 = Release|Any CPU
{BC9B81D5-0862-4465-9357-738BB0B2E4E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC9B81D5-0862-4465-9357-738BB0B2E4E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC9B81D5-0862-4465-9357-738BB0B2E4E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC9B81D5-0862-4465-9357-738BB0B2E4E0}.Release|Any CPU.Build.0 = Release|Any CPU
{8CCFF617-CDF1-4EFD-812E-AE92CD75F8CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CCFF617-CDF1-4EFD-812E-AE92CD75F8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CCFF617-CDF1-4EFD-812E-AE92CD75F8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CCFF617-CDF1-4EFD-812E-AE92CD75F8CA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{404532FF-0F8C-4A80-A182-3FEBAB789DC3} = {E54A5670-8E0E-463E-8FBF-430054BF2736}
{32D8C232-E015-4DC5-8DF6-D9B6C70D2BC4} = {E54A5670-8E0E-463E-8FBF-430054BF2736}
{44AEEFE7-C6CA-482D-BDDF-54F5CF570295} = {E54A5670-8E0E-463E-8FBF-430054BF2736}
{61D6B7B9-FB16-4D3C-B0FA-1EE6B5124E22} = {E54A5670-8E0E-463E-8FBF-430054BF2736}
{BC9B81D5-0862-4465-9357-738BB0B2E4E0} = {E54A5670-8E0E-463E-8FBF-430054BF2736}
{8CCFF617-CDF1-4EFD-812E-AE92CD75F8CA} = {A98D294C-F010-49CE-968F-A85E2F921692}
EndGlobalSection
EndGlobal
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="blue" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3B82F6"/>
<stop offset="1" stop-color="#1D4ED8"/>
</linearGradient>
</defs>
<rect x="8" y="8" width="240" height="240" rx="54" fill="url(#blue)"/>
<path d="M44 66h34l27 91 23-59h1l23 59 27-91h33l-38 126h-35l-11-31-12 31H82L44 66z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

+93
View File
@@ -0,0 +1,93 @@
[
{
"editionId": "Professional",
"displayName": "Windows 10/11 专业版",
"productKey": "W269N-WFGWX-YVC9B-4J6C9-T83GX",
"appliesTo": "Windows 10/11 Pro"
},
{
"editionId": "ProfessionalN",
"displayName": "Windows 10/11 专业版 N",
"productKey": "MH37W-N47XK-V7XM9-C7227-GCQG9",
"appliesTo": "Windows 10/11 Pro N"
},
{
"editionId": "ProfessionalWorkstation",
"displayName": "Windows 10/11 专业工作站版",
"productKey": "NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J",
"appliesTo": "Windows 10/11 Pro for Workstations"
},
{
"editionId": "ProfessionalWorkstationN",
"displayName": "Windows 10/11 专业工作站版 N",
"productKey": "9FNHH-K3HBT-3W4TD-6383H-6XYWF",
"appliesTo": "Windows 10/11 Pro for Workstations N"
},
{
"editionId": "ProfessionalEducation",
"displayName": "Windows 10/11 专业教育版",
"productKey": "6TP4R-GNPTD-KYYHQ-7B7DP-J447Y",
"appliesTo": "Windows 10/11 Pro Education"
},
{
"editionId": "ProfessionalEducationN",
"displayName": "Windows 10/11 专业教育版 N",
"productKey": "YVWGF-BXNMC-HTQYQ-CPQ99-66QFC",
"appliesTo": "Windows 10/11 Pro Education N"
},
{
"editionId": "Education",
"displayName": "Windows 10/11 教育版",
"productKey": "NW6C2-QMPVW-D7KKK-3GKT6-VCFB2",
"appliesTo": "Windows 10/11 Education"
},
{
"editionId": "EducationN",
"displayName": "Windows 10/11 教育版 N",
"productKey": "2WH4N-8QGBV-H22JP-CT43Q-MDWWJ",
"appliesTo": "Windows 10/11 Education N"
},
{
"editionId": "Enterprise",
"displayName": "Windows 10/11 企业版",
"productKey": "NPPR9-FWDCX-D2C8J-H872K-2YT43",
"appliesTo": "Windows 10/11 Enterprise"
},
{
"editionId": "EnterpriseN",
"displayName": "Windows 10/11 企业版 N",
"productKey": "DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4",
"appliesTo": "Windows 10/11 Enterprise N"
},
{
"editionId": "EnterpriseG",
"displayName": "Windows 10/11 企业版 G",
"productKey": "YYVX9-NTFWV-6MDM3-9PT4T-4M68B",
"appliesTo": "Windows 10/11 Enterprise G"
},
{
"editionId": "EnterpriseGN",
"displayName": "Windows 10/11 企业版 G N",
"productKey": "44RPN-FTY23-9VTTB-MP9BX-T84FV",
"appliesTo": "Windows 10/11 Enterprise G N"
},
{
"editionId": "EnterpriseS",
"displayName": "Windows 10/11 企业版 LTSC",
"productKey": "M7XTQ-FN8P6-TTKYV-9D4CC-J462D",
"appliesTo": "Enterprise LTSC 2019/2021/2024"
},
{
"editionId": "EnterpriseSN",
"displayName": "Windows 10/11 企业版 LTSC N",
"productKey": "92NFX-8DJQP-P6BBQ-THF9C-7CG2H",
"appliesTo": "Enterprise LTSC N 2019/2021/2024"
},
{
"editionId": "Core",
"displayName": "Windows 家庭版(不支持 KMS 客户端激活)",
"productKey": "",
"appliesTo": "Windows 10/11 Home",
"supported": false
}
]
+53
View File
@@ -0,0 +1,53 @@
# 架构说明
## 项目组成
```text
WindowsToolbox.App
WPF 界面、MVVM、UAC 客户端
WindowsToolbox.ElevatedHost
固定命令白名单、管理员权限系统修改
WindowsToolbox.Cli
当前用户桌面归档计划任务入口
WindowsToolbox.Infrastructure
注册表、进程、任务计划、快照、各功能实现
WindowsToolbox.Core
命令、状态、结果、快照和配置模型
WindowsToolbox.Verification
不修改系统的自动验证程序
```
## 执行流程
```text
用户点击功能
→ 主程序生成强类型 ToolboxCommand
→ 普通文件操作直接执行
→ 系统操作写入 Pending 目录并请求 UAC
→ ElevatedHost 验证路径、时效和命令类型
→ 首次操作保存基线快照
→ 执行固定实现
→ 读取实际状态进行验证
→ 写入结果和操作历史
→ 主程序刷新系统状态
```
管理员执行器只接受枚举中的命令以及经过各模块验证的参数,不提供任意 CMD、PowerShell 或脚本执行入口。
## 扩展新功能
新增功能时应按以下顺序:
1.`ToolboxCommandKind` 中添加固定命令。
2. 在基础设施项目中实现模块服务。
3. 确定原始状态的完整快照范围。
4.`ToolboxCommandDispatcher` 注册执行分支和提权要求。
5. 在 WPF ViewModel 暴露命令,在页面添加状态、风险说明、应用和恢复入口。
6. 向 Verification 项目增加非破坏性验证。
每个修改操作必须满足幂等、可验证和尽可能可恢复;不能只根据进程退出码显示成功。
+46
View File
@@ -0,0 +1,46 @@
# 系统操作边界
## 右键菜单与快捷方式箭头
经典菜单使用当前用户 Shell 类注册项,并在修改后由普通权限主程序自动重启 Explorer。检测到 Atlas OS 时,同时维护 Atlas Toolbox 使用的状态镜像,实际状态仍以 Explorer 读取的 CLSID 项为准。快捷方式箭头使用系统 `Shell Icons` 覆盖值和工具生成的透明 ICO。
## Windows 更新
强力关闭模式管理:
- Windows Update 自动更新策略。
- `wuauserv``UsoSvc``WaaSMedicSvc` 的可控制启动状态。
- Update Orchestrator 与 Windows Update 中存在且允许修改的计划任务。
- `WindowsToolbox\UpdateGuard` 每日守护任务。
部分服务和任务在不同 Windows 构建中受系统保护。模块会保留每一步结果,不把拒绝访问伪装成成功。
暂停模式写入开始和结束时间,最长限制为 35 天。自动续期任务每天重新请求所选暂停长度并再次验证。
## Windows 安全
模块使用 Defender 和防火墙的 Windows PowerShell Cmdlet。它不会停止 Security Center 核心服务,不会修改系统文件所有权,也不会绕过防篡改保护。恢复时按基线快照逐项恢复 Defender 偏好、三个防火墙配置和通知策略。
## 桌面归档
枚举桌面根目录中的文件和文件夹。默认排除:
- `desktop.ini`
- `.lnk``.url`
- 带 System 属性的文件或文件夹
- 重解析点、目录联接及其他可能指向桌面外部的特殊目录
普通隐藏文件会归档。虚拟的“此电脑”“回收站”等系统图标不属于文件系统枚举范围,因此不会移动。同名目标采用 `名称 (1).扩展名``文件夹 (1)` 递增。每次执行都生成恢复清单;撤销只移动仍然存在于归档目录中的项目。
## KMS
密钥目录位于 `data\kms-keys.json`,可以手动维护。激活顺序为:
1. 验证版本、密钥格式、主机和端口。
2. TCP 测试本地 KMS 主机。
3. 保存原 KMS 主机和端口。
4. 安装所选 GVLK。
5. 设置 KMS 主机。
6. 请求 Windows 激活。
如果连接测试失败,不修改产品密钥。
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "8.0.423",
"rollForward": "latestPatch"
}
}
+51
View File
@@ -0,0 +1,51 @@
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release',
[switch]$SkipRestore
)
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot
$dotnetExe = Join-Path $projectRoot '.tools\dotnet\dotnet.exe'
$nugetConfig = Join-Path $projectRoot 'NuGet.Config'
$outputRoot = Join-Path $projectRoot 'artifacts\WindowsToolbox'
if (-not (Test-Path -LiteralPath $dotnetExe)) {
throw 'Local .NET SDK was not found under .tools\dotnet.'
}
$env:DOTNET_CLI_HOME = Join-Path $projectRoot '.tools\dotnet-home'
$env:NUGET_PACKAGES = Join-Path $projectRoot '.tools\nuget'
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
if (-not $SkipRestore) {
& $dotnetExe restore (Join-Path $projectRoot 'WindowsToolbox.sln') --configfile $nugetConfig
if ($LASTEXITCODE -ne 0) { throw 'Restore failed.' }
}
$commonArguments = @(
'--configuration', $Configuration,
'--framework', 'net8.0-windows',
'--self-contained', 'false',
'--no-restore',
'--output', $outputRoot,
'-p:DebugType=None',
'-p:DebugSymbols=false'
)
& $dotnetExe publish (Join-Path $projectRoot 'src\WindowsToolbox.ElevatedHost\WindowsToolbox.ElevatedHost.csproj') @commonArguments
if ($LASTEXITCODE -ne 0) { throw 'Elevated host publish failed.' }
& $dotnetExe publish (Join-Path $projectRoot 'src\WindowsToolbox.Cli\WindowsToolbox.Cli.csproj') @commonArguments
if ($LASTEXITCODE -ne 0) { throw 'CLI publish failed.' }
& $dotnetExe publish (Join-Path $projectRoot 'src\WindowsToolbox.App\WindowsToolbox.App.csproj') @commonArguments
if ($LASTEXITCODE -ne 0) { throw 'WPF app publish failed.' }
Copy-Item -LiteralPath (Join-Path $projectRoot 'README.md') -Destination (Join-Path $outputRoot 'README.md') -Force
Write-Host "Published to: $outputRoot" -ForegroundColor Green
Write-Host 'Run WindowsToolbox.App.exe to start.' -ForegroundColor Green
+62
View File
@@ -0,0 +1,62 @@
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "assets"
SCALE = 4
SIZE = 256
CANVAS = SIZE * SCALE
def scaled_points(points):
return [(x * SCALE, y * SCALE) for x, y in points]
mask = Image.new("L", (CANVAS, CANVAS), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.rounded_rectangle(
(8 * SCALE, 8 * SCALE, 248 * SCALE, 248 * SCALE),
radius=54 * SCALE,
fill=255,
)
background = Image.new("RGBA", (CANVAS, CANVAS), (0, 0, 0, 0))
pixels = background.load()
for y in range(CANVAS):
ratio = y / max(1, CANVAS - 1)
red = round(59 + (29 - 59) * ratio)
green = round(130 + (78 - 130) * ratio)
blue = round(246 + (216 - 246) * ratio)
for x in range(CANVAS):
pixels[x, y] = (red, green, blue, mask.getpixel((x, y)))
draw = ImageDraw.Draw(background, "RGBA")
highlight = Image.new("RGBA", (CANVAS, CANVAS), (0, 0, 0, 0))
highlight_draw = ImageDraw.Draw(highlight, "RGBA")
highlight_draw.ellipse(
(-60 * SCALE, -110 * SCALE, 250 * SCALE, 150 * SCALE),
fill=(255, 255, 255, 22),
)
background.alpha_composite(highlight)
w_shape = [
(44, 66), (78, 66), (105, 157), (128, 98),
(129, 98), (152, 157), (179, 66), (212, 66),
(174, 192), (139, 192), (128, 161), (116, 192),
(82, 192),
]
shadow_shape = [(x, y + 4) for x, y in w_shape]
draw.polygon(scaled_points(shadow_shape), fill=(10, 35, 90, 45))
draw.polygon(scaled_points(w_shape), fill=(255, 255, 255, 255))
icon = background.resize((SIZE, SIZE), Image.Resampling.LANCZOS)
ASSETS.mkdir(parents=True, exist_ok=True)
icon.save(ASSETS / "WindowsToolbox.png", "PNG", optimize=True)
icon.save(
ASSETS / "WindowsToolbox.ico",
"ICO",
sizes=[(16, 16), (20, 20), (24, 24), (32, 32), (40, 40), (48, 48), (64, 64), (128, 128), (256, 256)],
)
print(ASSETS / "WindowsToolbox.ico")
+128
View File
@@ -0,0 +1,128 @@
<Application x:Class="WindowsToolbox.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WindowsToolbox.App"
StartupUri="MainWindow.xaml">
<Application.Resources>
<local:PageVisibilityConverter x:Key="PageVisibilityConverter" />
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<SolidColorBrush x:Key="BackgroundBrush" Color="#F3F6FA" />
<SolidColorBrush x:Key="SidebarBrush" Color="#111827" />
<SolidColorBrush x:Key="PrimaryBrush" Color="#2563EB" />
<SolidColorBrush x:Key="PrimaryHoverBrush" Color="#1D4ED8" />
<SolidColorBrush x:Key="DangerBrush" Color="#DC2626" />
<SolidColorBrush x:Key="DangerHoverBrush" Color="#B91C1C" />
<SolidColorBrush x:Key="TextBrush" Color="#172033" />
<SolidColorBrush x:Key="MutedTextBrush" Color="#64748B" />
<SolidColorBrush x:Key="BorderBrush" Color="#E2E8F0" />
<Style TargetType="Window">
<Setter Property="FontFamily" Value="Segoe UI Variable Text, Microsoft YaHei UI" />
<Setter Property="TextOptions.TextFormattingMode" Value="Display" />
<Setter Property="TextOptions.TextRenderingMode" Value="ClearType" />
</Style>
<Style x:Key="CardStyle" TargetType="Border">
<Setter Property="Background" Value="White" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="22" />
<Setter Property="Margin" Value="0,0,0,16" />
</Style>
<Style x:Key="PageTitleStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="28" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="Margin" Value="0,0,0,6" />
</Style>
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="18" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="Margin" Value="0,0,0,6" />
</Style>
<Style x:Key="DescriptionStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13" />
<Setter Property="Foreground" Value="{StaticResource MutedTextBrush}" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="LineHeight" Value="20" />
</Style>
<Style x:Key="BaseButtonStyle" TargetType="Button">
<Setter Property="MinHeight" Value="38" />
<Setter Property="Padding" Value="18,8" />
<Setter Property="Margin" Value="0,10,10,0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="ButtonBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ButtonBorder" Property="Opacity" Value="0.88" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="ButtonBorder" Property="Opacity" Value="0.72" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ButtonBorder" Property="Opacity" Value="0.45" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="PrimaryButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style x:Key="SecondaryButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="Background" Value="#EFF4FA" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style x:Key="DangerButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style x:Key="InputStyle" TargetType="TextBox">
<Setter Property="MinHeight" Value="38" />
<Setter Property="Padding" Value="10,7" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="#CBD5E1" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style TargetType="ComboBox">
<Setter Property="MinHeight" Value="38" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Margin" Value="0,10,0,0" />
</Style>
</Application.Resources>
</Application>
+12
View File
@@ -0,0 +1,12 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace WindowsToolbox.App;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+16
View File
@@ -0,0 +1,16 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace WindowsToolbox.App;
public sealed class PageVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
string.Equals(value?.ToString(), parameter?.ToString(), StringComparison.OrdinalIgnoreCase)
? Visibility.Visible
: Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
Binding.DoNothing;
}
+351
View File
@@ -0,0 +1,351 @@
<Window x:Class="WindowsToolbox.App.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Windows 工具箱"
Icon="/Assets/WindowsToolbox.ico"
Width="1180" Height="780" MinWidth="1000" MinHeight="680"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BackgroundBrush}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="238" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource SidebarBrush}">
<Grid Margin="18,24,18,18">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Margin="8,0,8,28">
<Image Source="/Assets/WindowsToolbox.ico" Width="46" Height="46" HorizontalAlignment="Left" />
<TextBlock Text="Windows 工具箱" Foreground="White" FontSize="20" FontWeight="SemiBold" Margin="0,14,0,2" />
<TextBlock Text="系统设置 · 可恢复" Foreground="#94A3B8" FontSize="12" />
</StackPanel>
<ItemsControl Grid.Row="1" ItemsSource="{Binding Navigation}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.NavigateCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}" Cursor="Hand" Padding="13,11" Margin="0,2"
HorizontalContentAlignment="Stretch" BorderThickness="0">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="#CBD5E1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="NavBorder" Background="{TemplateBinding Background}" CornerRadius="8" Padding="{TemplateBinding Padding}">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="NavBorder" Property="Background" Value="#1E293B" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="Background" Value="#2563EB" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Glyph}" FontSize="16" Foreground="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}" />
<TextBlock Grid.Column="1" Text="{Binding Title}" FontSize="14" FontWeight="SemiBold"
Foreground="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}" />
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border Grid.Row="2" Background="#1E293B" CornerRadius="8" Padding="12">
<StackPanel>
<TextBlock Text="当前系统" Foreground="#94A3B8" FontSize="11" />
<TextBlock Text="{Binding OsSummary}" Foreground="White" FontSize="12" TextWrapping="Wrap" Margin="0,4,0,0" />
</StackPanel>
</Border>
</Grid>
</Border>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<Grid Margin="34,28,34,24">
<!-- Dashboard -->
<StackPanel Visibility="{Binding SelectedPage, Converter={StaticResource PageVisibilityConverter}, ConverterParameter=Dashboard}">
<DockPanel Margin="0,0,0,22">
<StackPanel DockPanel.Dock="Left">
<TextBlock Text="系统概览" Style="{StaticResource PageTitleStyle}" />
<TextBlock Text="实时读取系统状态;修改后会重新验证。" Style="{StaticResource DescriptionStyle}" />
</StackPanel>
<Button DockPanel.Dock="Right" Content="刷新状态" Command="{Binding RefreshCommand}"
Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Right" Margin="12,0,0,0" />
</DockPanel>
<UniformGrid Columns="2">
<Border Style="{StaticResource CardStyle}" Margin="0,0,8,16">
<StackPanel>
<TextBlock Text="操作系统" Foreground="{StaticResource MutedTextBrush}" />
<TextBlock Text="{Binding OsSummary}" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TextBrush}" Margin="0,8,0,0" />
<TextBlock Text="{Binding EditionSummary}" Style="{StaticResource DescriptionStyle}" Margin="0,4,0,0" />
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}" Margin="8,0,0,16">
<StackPanel>
<TextBlock Text="Windows 更新" Foreground="{StaticResource MutedTextBrush}" />
<TextBlock Text="{Binding UpdateStatusText}" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TextBrush}" Margin="0,8,0,0" />
<TextBlock Text="策略、暂停时间和守护任务会分别记录" Style="{StaticResource DescriptionStyle}" Margin="0,4,0,0" />
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}" Margin="0,0,8,16">
<StackPanel>
<TextBlock Text="Defender" Foreground="{StaticResource MutedTextBrush}" />
<TextBlock Text="{Binding DefenderStatusText}" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TextBrush}" Margin="0,8,0,0" />
<TextBlock Text="{Binding TamperStatusText}" Style="{StaticResource DescriptionStyle}" Margin="0,4,0,0" />
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}" Margin="8,0,0,16">
<StackPanel>
<TextBlock Text="Windows 激活" Foreground="{StaticResource MutedTextBrush}" />
<TextBlock Text="{Binding ActivationStatusText}" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TextBrush}" Margin="0,8,0,0" />
<TextBlock Text="{Binding KmsStatusText}" Style="{StaticResource DescriptionStyle}" Margin="0,4,0,0" />
</StackPanel>
</Border>
</UniformGrid>
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="最近一次操作" Style="{StaticResource SectionTitleStyle}" />
<TextBlock Text="{Binding LastMessage}" Style="{StaticResource DescriptionStyle}" />
<ItemsControl ItemsSource="{Binding LastSteps}" Margin="0,10,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="0,4">
<Grid.ColumnDefinitions><ColumnDefinition Width="20" /><ColumnDefinition Width="150" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions>
<TextBlock Text="{Binding StatusGlyph}" Foreground="{Binding StatusBrush}" FontWeight="Bold" />
<TextBlock Grid.Column="1" Text="{Binding Name}" FontWeight="SemiBold" Foreground="{StaticResource TextBrush}" />
<TextBlock Grid.Column="2" Text="{Binding Message}" TextWrapping="Wrap" Foreground="{StaticResource MutedTextBrush}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
<!-- Appearance -->
<StackPanel Visibility="{Binding SelectedPage, Converter={StaticResource PageVisibilityConverter}, ConverterParameter=Appearance}">
<TextBlock Text="外观与资源管理器" Style="{StaticResource PageTitleStyle}" />
<TextBlock Text="修改后可统一重启资源管理器使设置生效。" Style="{StaticResource DescriptionStyle}" Margin="0,0,0,22" />
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="Windows 11 经典右键菜单" Style="{StaticResource SectionTitleStyle}" />
<TextBlock Text="恢复 Windows 10 风格的完整右键菜单;仅 Windows 11 使用该设置。" Style="{StaticResource DescriptionStyle}" />
<WrapPanel>
<Button Content="启用经典菜单" Command="{Binding EnableClassicMenuCommand}" Style="{StaticResource PrimaryButtonStyle}" />
<Button Content="恢复 Windows 11 菜单" Command="{Binding RestoreContextMenuCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="快捷方式小箭头" Style="{StaticResource SectionTitleStyle}" />
<TextBlock Text="使用工具生成的透明图标覆盖快捷方式箭头,并保存原始注册表值。" Style="{StaticResource DescriptionStyle}" />
<WrapPanel>
<Button Content="隐藏小箭头" Command="{Binding HideShortcutArrowCommand}" Style="{StaticResource PrimaryButtonStyle}" />
<Button Content="恢复小箭头" Command="{Binding RestoreShortcutArrowCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
<Button Content="重启资源管理器" Command="{Binding RestartExplorerCommand}" Style="{StaticResource SecondaryButtonStyle}" HorizontalAlignment="Left" />
</StackPanel>
<!-- Updates -->
<StackPanel Visibility="{Binding SelectedPage, Converter={StaticResource PageVisibilityConverter}, ConverterParameter=Updates}">
<TextBlock Text="Windows 更新" Style="{StaticResource PageTitleStyle}" />
<TextBlock Text="暂停和强力关闭共用一份原始状态快照;恢复会回到首次修改前。" Style="{StaticResource DescriptionStyle}" Margin="0,0,0,22" />
<Border Style="{StaticResource CardStyle}">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="230" /></Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="暂停更新" Style="{StaticResource SectionTitleStyle}" />
<TextBlock Text="单次最长 35 天;可创建每日检查任务自动续期。" Style="{StaticResource DescriptionStyle}" />
<WrapPanel>
<Button Content="立即暂停" Command="{Binding PauseUpdateCommand}" Style="{StaticResource PrimaryButtonStyle}" />
<Button Content="应用自动续期设置" Command="{Binding ConfigureUpdateRenewalCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</WrapPanel>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="暂停天数" Foreground="{StaticResource MutedTextBrush}" />
<ComboBox ItemsSource="{Binding PauseDayOptions}" SelectedItem="{Binding SelectedPauseDays}" Margin="0,6,0,4" />
<CheckBox Content="每天自动检查并续期" IsChecked="{Binding AutoRenewEnabled}" />
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource CardStyle}" BorderBrush="#FECACA" Background="#FFF7F7">
<StackPanel>
<TextBlock Text="强力关闭自动更新" Style="{StaticResource SectionTitleStyle}" Foreground="#991B1B" />
<TextBlock Text="设置禁止更新策略,关闭可控制的更新服务和任务,并创建每日守护任务。受保护组件若被系统拒绝,会在操作详情中标明。" Style="{StaticResource DescriptionStyle}" />
<WrapPanel>
<Button Content="强力关闭更新" Command="{Binding DisableUpdateCommand}" Style="{StaticResource DangerButtonStyle}" />
<Button Content="恢复更新原始状态" Command="{Binding RestoreUpdateCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
</StackPanel>
<!-- Security -->
<StackPanel Visibility="{Binding SelectedPage, Converter={StaticResource PageVisibilityConverter}, ConverterParameter=Security}">
<TextBlock Text="安全与登录" Style="{StaticResource PageTitleStyle}" />
<TextBlock Text="只使用 Windows 可控制的策略和 Cmdlet,不绕过防篡改或 TrustedInstaller。" Style="{StaticResource DescriptionStyle}" Margin="0,0,0,22" />
<Border Style="{StaticResource CardStyle}" BorderBrush="#FECACA" Background="#FFF7F7">
<StackPanel>
<TextBlock Text="Defender 防护" Style="{StaticResource SectionTitleStyle}" Foreground="#991B1B" />
<TextBlock Text="执行前需要在 Windows 安全中心手动关闭防篡改保护。工具会保存 Defender、通知及防火墙原始设置。" Style="{StaticResource DescriptionStyle}" />
<CheckBox Content="同时关闭 Domain、Public、Private 三个防火墙配置" IsChecked="{Binding DisableFirewall}" />
<WrapPanel>
<Button Content="关闭所选防护" Command="{Binding DisableSecurityCommand}" Style="{StaticResource DangerButtonStyle}" />
<Button Content="恢复安全设置" Command="{Binding RestoreSecurityCommand}" Style="{StaticResource SecondaryButtonStyle}" />
<Button Content="打开 Windows 安全中心" Command="{Binding OpenWindowsSecurityCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="免密码自动登录" Style="{StaticResource SectionTitleStyle}" />
<TextBlock Text="恢复 netplwiz 中可能被隐藏的账户密码复选框,然后由你在 Windows 自带窗口中完成设置。工具不会读取或保存密码。" Style="{StaticResource DescriptionStyle}" />
<WrapPanel>
<Button Content="准备并打开账户控制" Command="{Binding PrepareAutoLogonCommand}" Style="{StaticResource PrimaryButtonStyle}" />
<Button Content="恢复选项注册表" Command="{Binding RestoreAutoLogonCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
</StackPanel>
<!-- Desktop -->
<StackPanel Visibility="{Binding SelectedPage, Converter={StaticResource PageVisibilityConverter}, ConverterParameter=Desktop}">
<TextBlock Text="桌面文件归档" Style="{StaticResource PageTitleStyle}" />
<TextBlock Text="归档到 yyyy-MM-dd 子目录;包含普通文件夹,仅排除快捷方式和系统项。" Style="{StaticResource DescriptionStyle}" Margin="0,0,0,22" />
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="目标位置" Style="{StaticResource SectionTitleStyle}" />
<Grid Margin="0,8,0,4">
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
<TextBox Text="{Binding DesktopDestination, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource InputStyle}" />
<Button Grid.Column="1" Content="选择文件夹" Command="{Binding BrowseDesktopDestinationCommand}" Style="{StaticResource SecondaryButtonStyle}" Margin="10,0,0,0" />
</Grid>
<WrapPanel>
<Button Content="立即归档" Command="{Binding ArchiveDesktopCommand}" Style="{StaticResource PrimaryButtonStyle}" />
<Button Content="撤销最近一次归档" Command="{Binding UndoDesktopArchiveCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="每月自动执行" Style="{StaticResource SectionTitleStyle}" />
<TextBlock Text="使用当前登录用户的任务计划;电脑关机错过时间后会在下次可用时执行。" Style="{StaticResource DescriptionStyle}" />
<WrapPanel Margin="0,10,0,0">
<StackPanel Width="180" Margin="0,0,14,0">
<TextBlock Text="每月日期(1—28" Foreground="{StaticResource MutedTextBrush}" />
<ComboBox ItemsSource="{Binding MonthlyDayOptions}" SelectedItem="{Binding SelectedMonthlyDay}" Margin="0,6,0,0" />
</StackPanel>
<StackPanel Width="180">
<TextBlock Text="执行时间(HH:mm" Foreground="{StaticResource MutedTextBrush}" />
<TextBox Text="{Binding ScheduleTime}" Style="{StaticResource InputStyle}" Margin="0,6,0,0" />
</StackPanel>
</WrapPanel>
<CheckBox Content="启用每月自动归档" IsChecked="{Binding DesktopScheduleEnabled}" />
<Button Content="保存计划任务" Command="{Binding ConfigureDesktopScheduleCommand}" Style="{StaticResource PrimaryButtonStyle}" HorizontalAlignment="Left" />
</StackPanel>
</Border>
</StackPanel>
<!-- Activation -->
<StackPanel Visibility="{Binding SelectedPage, Converter={StaticResource PageVisibilityConverter}, ConverterParameter=Activation}">
<TextBlock Text="Windows KMS 激活" Style="{StaticResource PageTitleStyle}" />
<TextBlock Text="内置微软公开 GVLK;KMS 主机由你填写并在连接成功后才安装密钥。" Style="{StaticResource DescriptionStyle}" Margin="0,0,0,22" />
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="Windows 版本" Foreground="{StaticResource MutedTextBrush}" />
<ComboBox ItemsSource="{Binding KmsKeys}" SelectedItem="{Binding SelectedKmsKey}" DisplayMemberPath="DisplayName" Margin="0,6,0,14" />
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="130" /></Grid.ColumnDefinitions>
<StackPanel Margin="0,0,12,0">
<TextBlock Text="本地 KMS 主机" Foreground="{StaticResource MutedTextBrush}" />
<TextBox Text="{Binding KmsHost, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource InputStyle}" Margin="0,6,0,0" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="端口" Foreground="{StaticResource MutedTextBrush}" />
<TextBox Text="{Binding KmsPort, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource InputStyle}" Margin="0,6,0,0" />
</StackPanel>
</Grid>
<Border Background="#FFF7ED" BorderBrush="#FED7AA" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,16,0,4">
<TextBlock Text="提示:Windows 客户端 KMS 需要服务器达到 25 个不同客户端的激活门槛。替换产品密钥后,Windows 无法导出并自动恢复旧完整密钥。" TextWrapping="Wrap" Foreground="#9A3412" />
</Border>
<WrapPanel>
<Button Content="连接并激活" Command="{Binding ActivateWindowsCommand}" Style="{StaticResource PrimaryButtonStyle}" />
<Button Content="恢复原 KMS 主机" Command="{Binding RestoreKmsCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
</StackPanel>
<!-- History -->
<StackPanel Visibility="{Binding SelectedPage, Converter={StaticResource PageVisibilityConverter}, ConverterParameter=History}">
<DockPanel Margin="0,0,0,22">
<StackPanel DockPanel.Dock="Left">
<TextBlock Text="操作历史" Style="{StaticResource PageTitleStyle}" />
<TextBlock Text="最近 100 次操作及执行结果。" Style="{StaticResource DescriptionStyle}" />
</StackPanel>
<Button DockPanel.Dock="Right" Content="刷新历史" Command="{Binding RefreshHistoryCommand}" Style="{StaticResource SecondaryButtonStyle}" />
</DockPanel>
<ItemsControl ItemsSource="{Binding History}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource CardStyle}" Padding="16" Margin="0,0,0,10">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="145" /><ColumnDefinition Width="190" /><ColumnDefinition Width="*" /><ColumnDefinition Width="70" /></Grid.ColumnDefinitions>
<TextBlock Text="{Binding TimestampText}" Foreground="{StaticResource MutedTextBrush}" />
<TextBlock Grid.Column="1" Text="{Binding Action}" Foreground="{StaticResource TextBrush}" FontWeight="SemiBold" />
<TextBlock Grid.Column="2" Text="{Binding Summary}" Foreground="{StaticResource MutedTextBrush}" TextWrapping="Wrap" />
<TextBlock Grid.Column="3" Text="{Binding StatusText}" Foreground="{Binding StatusBrush}" FontWeight="SemiBold" HorizontalAlignment="Right" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</ScrollViewer>
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,1,0,0" Padding="18,10">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
<Ellipse Width="8" Height="8" Fill="{Binding StatusBrush}" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Text="{Binding LastMessage}" Foreground="{StaticResource MutedTextBrush}" Margin="9,0,0,0" TextTrimming="CharacterEllipsis" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Visibility="{Binding IsBusy, Converter={StaticResource BooleanToVisibilityConverter}}">
<ProgressBar Width="120" Height="4" IsIndeterminate="True" Margin="0,0,10,0" />
<TextBlock Text="正在执行…" Foreground="{StaticResource MutedTextBrush}" />
</StackPanel>
</Grid>
</Border>
</Grid>
</Grid>
</Window>
+21
View File
@@ -0,0 +1,21 @@
using System.Windows;
namespace WindowsToolbox.App;
public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel = new();
public MainWindow()
{
InitializeComponent();
DataContext = _viewModel;
Loaded += OnLoaded;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
Loaded -= OnLoaded;
await _viewModel.InitializeAsync();
}
}
+550
View File
@@ -0,0 +1,550 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Win32;
using WindowsToolbox.Core;
using WindowsToolbox.Infrastructure;
namespace WindowsToolbox.App;
public enum ToolboxPage
{
Dashboard,
Appearance,
Updates,
Security,
Desktop,
Activation,
History
}
public sealed class MainViewModel : ObservableObject
{
private readonly ElevatedCommandClient _commands = new();
private readonly SystemStatusService _statusService = new(new RegistryStore(), new ProcessRunner());
private readonly OperationLogStore _logStore = new();
private readonly ActivationService _activationService = new(new RegistryStore(), new SnapshotStore(), new ProcessRunner());
private SystemStatus? _status;
private ToolboxPage _selectedPage;
private bool _isBusy;
private string _lastMessage = "准备读取系统状态…";
private bool? _lastSucceeded;
private int _selectedPauseDays = 35;
private bool _autoRenewEnabled;
private bool _disableFirewall;
private string _desktopDestination;
private bool _desktopScheduleEnabled;
private int _selectedMonthlyDay = 1;
private string _scheduleTime = "09:00";
private KmsKeyEntry? _selectedKmsKey;
private string _kmsHost = "127.0.0.1";
private string _kmsPort = "1688";
public MainViewModel()
{
var preferredDestination = Directory.Exists(@"D:\")
? @"D:\desktop"
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "DesktopArchive");
_desktopDestination = preferredDestination;
Navigation =
[
new("系统概览", "⌂", ToolboxPage.Dashboard, true),
new("外观与资源管理器", "◈", ToolboxPage.Appearance),
new("Windows 更新", "↻", ToolboxPage.Updates),
new("安全与登录", "◇", ToolboxPage.Security),
new("桌面归档", "▣", ToolboxPage.Desktop),
new("KMS 激活", "✓", ToolboxPage.Activation),
new("操作历史", "≡", ToolboxPage.History)
];
KmsKeys = new ObservableCollection<KmsKeyEntry>(_activationService.LoadCatalog());
_selectedKmsKey = KmsKeys.FirstOrDefault(x => x.EditionId == "Professional") ?? KmsKeys.FirstOrDefault();
NavigateCommand = new RelayCommand(parameter => Navigate((NavigationItemViewModel)parameter!));
RefreshCommand = new AsyncRelayCommand(_ => RefreshStatusAsync(), () => !IsBusy);
RefreshHistoryCommand = new AsyncRelayCommand(_ => RefreshHistoryAsync(), () => !IsBusy);
EnableClassicMenuCommand = Action(ToolboxCommandKind.EnableClassicContextMenu);
RestoreContextMenuCommand = Action(ToolboxCommandKind.RestoreContextMenu);
HideShortcutArrowCommand = Action(ToolboxCommandKind.HideShortcutArrow);
RestoreShortcutArrowCommand = Action(ToolboxCommandKind.RestoreShortcutArrow);
PauseUpdateCommand = new AsyncRelayCommand(_ => ExecuteAsync(
ToolboxCommandKind.PauseWindowsUpdate,
new() { ["days"] = SelectedPauseDays.ToString() }), () => !IsBusy);
ConfigureUpdateRenewalCommand = new AsyncRelayCommand(_ => ExecuteAsync(
ToolboxCommandKind.ConfigureUpdateRenewal,
new() { ["enabled"] = AutoRenewEnabled.ToString(), ["days"] = SelectedPauseDays.ToString() }), () => !IsBusy);
DisableUpdateCommand = new AsyncRelayCommand(_ => ExecuteAsync(
ToolboxCommandKind.DisableWindowsUpdate,
confirmation: "强力关闭会停止更新服务、禁用可控制的更新任务并创建每日守护任务。确定继续吗?"), () => !IsBusy);
RestoreUpdateCommand = Action(ToolboxCommandKind.RestoreWindowsUpdate);
DisableSecurityCommand = new AsyncRelayCommand(_ => ExecuteAsync(
ToolboxCommandKind.DisableDefenderProtection,
new() { ["firewall"] = DisableFirewall.ToString() },
DisableFirewall
? "这会关闭 Defender 防护及三个 Windows 防火墙配置。确定已经关闭防篡改保护并继续吗?"
: "这会关闭 Defender 的实时、行为、脚本和云端防护。确定已经关闭防篡改保护并继续吗?"), () => !IsBusy);
RestoreSecurityCommand = Action(ToolboxCommandKind.RestoreDefenderProtection);
PrepareAutoLogonCommand = new AsyncRelayCommand(async _ =>
{
var result = await ExecuteAsync(ToolboxCommandKind.PrepareAutoLogon,
confirmation: "自动登录会降低本机物理访问安全性。工具不会保存密码,是否继续打开 Windows 账户控制?");
if (result?.Success == true)
{
AutoLogonService.OpenNetplwiz();
}
}, () => !IsBusy);
RestoreAutoLogonCommand = Action(ToolboxCommandKind.RestoreAutoLogonPreparation);
BrowseDesktopDestinationCommand = new RelayCommand(_ => BrowseDestination());
ArchiveDesktopCommand = new AsyncRelayCommand(_ => ExecuteAsync(
ToolboxCommandKind.ArchiveDesktop, DesktopArguments()), () => !IsBusy);
UndoDesktopArchiveCommand = new AsyncRelayCommand(_ => ExecuteAsync(
ToolboxCommandKind.UndoDesktopArchive,
confirmation: "将按最近一次恢复清单把仍在归档目录中的文件移回桌面。确定继续吗?"), () => !IsBusy);
ConfigureDesktopScheduleCommand = new AsyncRelayCommand(_ => ExecuteAsync(
ToolboxCommandKind.ConfigureDesktopSchedule,
DesktopArguments(includeSchedule: true)), () => !IsBusy);
ActivateWindowsCommand = new AsyncRelayCommand(_ => ActivateAsync(), () => !IsBusy);
RestoreKmsCommand = new AsyncRelayCommand(_ => ExecuteAsync(
ToolboxCommandKind.RestoreKmsConfiguration,
confirmation: "这会恢复或清除原 KMS 主机配置,但无法恢复未知的旧完整产品密钥。确定继续吗?"), () => !IsBusy);
RestartExplorerCommand = new AsyncRelayCommand(_ => RestartExplorerAsync(), () => !IsBusy);
OpenWindowsSecurityCommand = new RelayCommand(_ => OpenWindowsSecurity());
}
public ObservableCollection<NavigationItemViewModel> Navigation { get; }
public ObservableCollection<KmsKeyEntry> KmsKeys { get; }
public ObservableCollection<StepViewModel> LastSteps { get; } = [];
public ObservableCollection<HistoryItemViewModel> History { get; } = [];
public IReadOnlyList<int> PauseDayOptions { get; } = [7, 14, 21, 28, 35];
public IReadOnlyList<int> MonthlyDayOptions { get; } = Enumerable.Range(1, 28).ToArray();
public ICommand NavigateCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand RefreshHistoryCommand { get; }
public ICommand EnableClassicMenuCommand { get; }
public ICommand RestoreContextMenuCommand { get; }
public ICommand HideShortcutArrowCommand { get; }
public ICommand RestoreShortcutArrowCommand { get; }
public ICommand PauseUpdateCommand { get; }
public ICommand ConfigureUpdateRenewalCommand { get; }
public ICommand DisableUpdateCommand { get; }
public ICommand RestoreUpdateCommand { get; }
public ICommand DisableSecurityCommand { get; }
public ICommand RestoreSecurityCommand { get; }
public ICommand PrepareAutoLogonCommand { get; }
public ICommand RestoreAutoLogonCommand { get; }
public ICommand BrowseDesktopDestinationCommand { get; }
public ICommand ArchiveDesktopCommand { get; }
public ICommand UndoDesktopArchiveCommand { get; }
public ICommand ConfigureDesktopScheduleCommand { get; }
public ICommand ActivateWindowsCommand { get; }
public ICommand RestoreKmsCommand { get; }
public ICommand RestartExplorerCommand { get; }
public ICommand OpenWindowsSecurityCommand { get; }
public ToolboxPage SelectedPage
{
get => _selectedPage;
private set => SetField(ref _selectedPage, value);
}
public bool IsBusy
{
get => _isBusy;
private set
{
if (SetField(ref _isBusy, value))
{
CommandManager.InvalidateRequerySuggested();
}
}
}
public string LastMessage
{
get => _lastMessage;
private set => SetField(ref _lastMessage, value);
}
public Brush StatusBrush => _lastSucceeded switch
{
true => Brushes.SeaGreen,
false => Brushes.Crimson,
null => Brushes.SlateGray
};
public string OsSummary => _status is null ? "正在检测…" : $"{_status.ProductName} {_status.DisplayVersion}";
public string EditionSummary => _status is null ? string.Empty : $"{_status.EditionId} · Build {_status.Build} · {(_status.IsAdministrator ? "" : "")}";
public string UpdateStatusText => _status?.UpdateMode ?? "正在检测…";
public string DefenderStatusText => _status?.DefenderRealtimeProtectionEnabled switch { true => "实时防护开启", false => "实时防护关闭", _ => "状态不可用" };
public string TamperStatusText => _status?.TamperProtectionEnabled switch { true => "防篡改保护:开启", false => "防篡改保护:关闭", _ => "防篡改保护状态不可用" };
public string ActivationStatusText => _status?.ActivationStatus ?? "正在检测…";
public string KmsStatusText => _status is null ? string.Empty : $"KMS{(_status.KmsHost ?? "")} · 密钥尾号:{(_status.PartialProductKey ?? "")}";
public int SelectedPauseDays { get => _selectedPauseDays; set => SetField(ref _selectedPauseDays, value); }
public bool AutoRenewEnabled { get => _autoRenewEnabled; set => SetField(ref _autoRenewEnabled, value); }
public bool DisableFirewall { get => _disableFirewall; set => SetField(ref _disableFirewall, value); }
public string DesktopDestination { get => _desktopDestination; set => SetField(ref _desktopDestination, value); }
public bool DesktopScheduleEnabled { get => _desktopScheduleEnabled; set => SetField(ref _desktopScheduleEnabled, value); }
public int SelectedMonthlyDay { get => _selectedMonthlyDay; set => SetField(ref _selectedMonthlyDay, value); }
public string ScheduleTime { get => _scheduleTime; set => SetField(ref _scheduleTime, value); }
public KmsKeyEntry? SelectedKmsKey { get => _selectedKmsKey; set => SetField(ref _selectedKmsKey, value); }
public string KmsHost { get => _kmsHost; set => SetField(ref _kmsHost, value); }
public string KmsPort { get => _kmsPort; set => SetField(ref _kmsPort, value); }
public async Task InitializeAsync()
{
await Task.WhenAll(
RefreshStatusAsync(showBusy: false),
RefreshHistoryAsync());
}
private AsyncRelayCommand Action(ToolboxCommandKind kind) =>
new(_ => ExecuteAsync(kind), () => !IsBusy);
private void Navigate(NavigationItemViewModel item)
{
foreach (var navigationItem in Navigation)
{
navigationItem.IsSelected = ReferenceEquals(navigationItem, item);
}
SelectedPage = item.Page;
}
private async Task<CommandResult?> ExecuteAsync(
ToolboxCommandKind kind,
Dictionary<string, string>? arguments = null,
string? confirmation = null)
{
if (!string.IsNullOrWhiteSpace(confirmation) && MessageBox.Show(
confirmation, "请确认", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes)
{
return null;
}
IsBusy = true;
LastMessage = "正在执行,请在出现 UAC 提示时确认…";
_lastSucceeded = null;
OnPropertyChanged(nameof(StatusBrush));
try
{
var result = await _commands.ExecuteAsync(kind, arguments);
ApplyResult(result);
if (result.Success && result.Steps.Any(step => step.RequiresRestart))
{
var restarted = await RestartExplorerCoreAsync();
LastSteps.Add(new StepViewModel(new CommandStepResult(
"重启资源管理器",
restarted,
restarted ? "已自动重启,设置现在生效" : "自动重启失败,请点击页面中的“重启资源管理器”")));
LastMessage = restarted ? result.Summary + ",资源管理器已自动重启" : result.Summary + ",但资源管理器重启失败";
_lastSucceeded = restarted;
OnPropertyChanged(nameof(StatusBrush));
}
var refreshAdvanced = kind is ToolboxCommandKind.DisableDefenderProtection or
ToolboxCommandKind.RestoreDefenderProtection or
ToolboxCommandKind.ActivateWindowsKms or
ToolboxCommandKind.RestoreKmsConfiguration;
await RefreshStatusAsync(preserveMessage: true, includeAdvanced: refreshAdvanced);
await RefreshHistoryAsync();
return result;
}
catch (Exception ex)
{
LastMessage = ex.Message;
_lastSucceeded = false;
OnPropertyChanged(nameof(StatusBrush));
return null;
}
finally
{
IsBusy = false;
}
}
private async Task RefreshStatusAsync(bool preserveMessage = false, bool showBusy = true, bool includeAdvanced = true)
{
var wasBusy = IsBusy;
if (showBusy)
{
IsBusy = true;
}
try
{
_status = await _statusService.DetectAsync(includeAdvanced, _status);
if (!preserveMessage)
{
LastMessage = "系统状态已刷新";
_lastSucceeded = true;
OnPropertyChanged(nameof(StatusBrush));
}
OnPropertyChanged(nameof(OsSummary));
OnPropertyChanged(nameof(EditionSummary));
OnPropertyChanged(nameof(UpdateStatusText));
OnPropertyChanged(nameof(DefenderStatusText));
OnPropertyChanged(nameof(TamperStatusText));
OnPropertyChanged(nameof(ActivationStatusText));
OnPropertyChanged(nameof(KmsStatusText));
if (!string.IsNullOrWhiteSpace(_status.KmsHost))
{
KmsHost = _status.KmsHost;
}
SelectedKmsKey = KmsKeys.FirstOrDefault(x => x.EditionId.Equals(_status.EditionId, StringComparison.OrdinalIgnoreCase))
?? SelectedKmsKey;
}
catch (Exception ex)
{
if (!preserveMessage)
{
LastMessage = "状态检测失败:" + ex.Message;
_lastSucceeded = false;
OnPropertyChanged(nameof(StatusBrush));
}
}
finally
{
if (showBusy)
{
IsBusy = wasBusy;
}
}
}
private async Task RefreshHistoryAsync()
{
var entries = await _logStore.ReadRecentAsync();
History.Clear();
foreach (var entry in entries)
{
History.Add(new HistoryItemViewModel(entry));
}
}
private void ApplyResult(CommandResult result)
{
LastMessage = result.Summary;
_lastSucceeded = result.Success;
OnPropertyChanged(nameof(StatusBrush));
LastSteps.Clear();
foreach (var step in result.Steps)
{
LastSteps.Add(new StepViewModel(step));
}
}
private Dictionary<string, string> DesktopArguments(bool includeSchedule = false)
{
var arguments = new Dictionary<string, string>
{
["profile"] = "default",
["destination"] = DesktopDestination
};
if (includeSchedule)
{
arguments["enabled"] = DesktopScheduleEnabled.ToString();
arguments["day"] = SelectedMonthlyDay.ToString();
arguments["time"] = ScheduleTime;
}
return arguments;
}
private async Task ActivateAsync()
{
if (SelectedKmsKey is null)
{
MessageBox.Show("请选择 Windows 版本。", "KMS 激活", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
await ExecuteAsync(ToolboxCommandKind.ActivateWindowsKms,
new()
{
["edition"] = SelectedKmsKey.EditionId,
["host"] = KmsHost,
["port"] = KmsPort
},
$"将安装“{SelectedKmsKey.DisplayName}”的 GVLK,并连接 {KmsHost}:{KmsPort}。确定继续吗?");
}
private void BrowseDestination()
{
var dialog = new OpenFolderDialog
{
Title = "选择桌面归档根目录",
Multiselect = false,
InitialDirectory = Directory.Exists(DesktopDestination) ? DesktopDestination : null
};
if (dialog.ShowDialog() == true)
{
DesktopDestination = dialog.FolderName;
}
}
private async Task RestartExplorerAsync()
{
if (MessageBox.Show("资源管理器窗口和任务栏会短暂关闭并自动重新启动。确定继续吗?",
"重启资源管理器", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
{
return;
}
IsBusy = true;
try
{
var restarted = await RestartExplorerCoreAsync();
LastMessage = restarted ? "资源管理器已重新启动" : "资源管理器重启失败";
_lastSucceeded = restarted;
OnPropertyChanged(nameof(StatusBrush));
}
catch (Exception ex)
{
LastMessage = "重启资源管理器失败:" + ex.Message;
_lastSucceeded = false;
OnPropertyChanged(nameof(StatusBrush));
}
finally
{
IsBusy = false;
}
}
private static async Task<bool> RestartExplorerCoreAsync()
{
try
{
var runner = new ProcessRunner();
await runner.RunAsync("ie4uinit.exe", ["-ClearIconCache"]);
await runner.RunAsync("taskkill.exe", ["/F", "/IM", "explorer.exe"]);
await Task.Delay(350);
return Process.Start(new ProcessStartInfo("explorer.exe") { UseShellExecute = true }) is not null;
}
catch
{
return false;
}
}
private static void OpenWindowsSecurity()
{
Process.Start(new ProcessStartInfo("windowsdefender:") { UseShellExecute = true });
}
}
public sealed class NavigationItemViewModel(string title, string glyph, ToolboxPage page, bool isSelected = false) : ObservableObject
{
private bool _isSelected = isSelected;
public string Title { get; } = title;
public string Glyph { get; } = glyph;
public ToolboxPage Page { get; } = page;
public bool IsSelected { get => _isSelected; set => SetField(ref _isSelected, value); }
}
public sealed class StepViewModel(CommandStepResult step)
{
public string Name { get; } = step.Name;
public string Message { get; } = step.Message;
public string StatusGlyph { get; } = step.Success ? "✓" : "×";
public Brush StatusBrush { get; } = step.Success ? Brushes.SeaGreen : Brushes.Crimson;
}
public sealed class HistoryItemViewModel(OperationLogEntry entry)
{
public string TimestampText { get; } = entry.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
public string Action { get; } = Localize(entry.Action);
public string Summary { get; } = entry.Summary;
public string StatusText { get; } = entry.Success ? "成功" : "失败";
public Brush StatusBrush { get; } = entry.Success ? Brushes.SeaGreen : Brushes.Crimson;
private static string Localize(string action) => Enum.TryParse<ToolboxCommandKind>(action, out var kind) ? kind switch
{
ToolboxCommandKind.EnableClassicContextMenu => "启用经典右键菜单",
ToolboxCommandKind.RestoreContextMenu => "恢复右键菜单",
ToolboxCommandKind.HideShortcutArrow => "隐藏快捷方式箭头",
ToolboxCommandKind.RestoreShortcutArrow => "恢复快捷方式箭头",
ToolboxCommandKind.PauseWindowsUpdate => "暂停 Windows 更新",
ToolboxCommandKind.DisableWindowsUpdate => "强力关闭 Windows 更新",
ToolboxCommandKind.RestoreWindowsUpdate => "恢复 Windows 更新",
ToolboxCommandKind.ConfigureUpdateRenewal => "更新暂停自动续期",
ToolboxCommandKind.DisableDefenderProtection => "关闭 Windows 防护",
ToolboxCommandKind.RestoreDefenderProtection => "恢复 Windows 防护",
ToolboxCommandKind.PrepareAutoLogon => "准备自动登录",
ToolboxCommandKind.RestoreAutoLogonPreparation => "恢复自动登录选项",
ToolboxCommandKind.ArchiveDesktop => "桌面归档",
ToolboxCommandKind.UndoDesktopArchive => "撤销桌面归档",
ToolboxCommandKind.ConfigureDesktopSchedule => "桌面归档计划",
ToolboxCommandKind.ActivateWindowsKms => "KMS 激活",
ToolboxCommandKind.RestoreKmsConfiguration => "恢复 KMS 配置",
_ => action
} : action;
}
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public sealed class RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null) : ICommand
{
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) => canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => execute(parameter);
}
public sealed class AsyncRelayCommand(Func<object?, Task> execute, Func<bool>? canExecute = null) : ICommand
{
private bool _running;
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) => !_running && (canExecute?.Invoke() ?? true);
public async void Execute(object? parameter)
{
if (!CanExecute(parameter))
{
return;
}
_running = true;
CommandManager.InvalidateRequerySuggested();
try
{
await execute(parameter);
}
finally
{
_running = false;
CommandManager.InvalidateRequerySuggested();
}
}
}
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\WindowsToolbox.Core\WindowsToolbox.Core.csproj" />
<ProjectReference Include="..\WindowsToolbox.Infrastructure\WindowsToolbox.Infrastructure.csproj" />
<ProjectReference Include="..\WindowsToolbox.ElevatedHost\WindowsToolbox.ElevatedHost.csproj" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\WindowsToolbox.Cli\WindowsToolbox.Cli.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\data\kms-keys.json" Link="data\kms-keys.json" CopyToOutputDirectory="PreserveNewest" />
<Resource Include="..\..\assets\WindowsToolbox.ico" Link="Assets\WindowsToolbox.ico" />
</ItemGroup>
<Target Name="CopyCompanionExecutables" AfterTargets="Build">
<ItemGroup>
<CompanionOutput Include="..\WindowsToolbox.ElevatedHost\bin\$(Configuration)\net8.0-windows\WindowsToolbox.ElevatedHost.exe" />
<CompanionOutput Include="..\WindowsToolbox.ElevatedHost\bin\$(Configuration)\net8.0-windows\WindowsToolbox.ElevatedHost.dll" />
<CompanionOutput Include="..\WindowsToolbox.ElevatedHost\bin\$(Configuration)\net8.0-windows\WindowsToolbox.ElevatedHost.deps.json" />
<CompanionOutput Include="..\WindowsToolbox.ElevatedHost\bin\$(Configuration)\net8.0-windows\WindowsToolbox.ElevatedHost.runtimeconfig.json" />
<CompanionOutput Include="..\WindowsToolbox.Cli\bin\$(Configuration)\net8.0-windows\WindowsToolbox.Cli.exe" />
<CompanionOutput Include="..\WindowsToolbox.Cli\bin\$(Configuration)\net8.0-windows\WindowsToolbox.Cli.dll" />
<CompanionOutput Include="..\WindowsToolbox.Cli\bin\$(Configuration)\net8.0-windows\WindowsToolbox.Cli.deps.json" />
<CompanionOutput Include="..\WindowsToolbox.Cli\bin\$(Configuration)\net8.0-windows\WindowsToolbox.Cli.runtimeconfig.json" />
</ItemGroup>
<Copy SourceFiles="@(CompanionOutput)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" />
</Target>
</Project>
+20
View File
@@ -0,0 +1,20 @@
using WindowsToolbox.Core;
using WindowsToolbox.Infrastructure;
if (args.Length >= 3 &&
args[0].Equals("desktop-archive", StringComparison.OrdinalIgnoreCase) &&
args[1].Equals("--profile", StringComparison.OrdinalIgnoreCase))
{
var dispatcher = new ToolboxCommandDispatcher();
var result = await dispatcher.ExecuteAsync(new ToolboxCommand
{
Id = Guid.NewGuid().ToString("N"),
Kind = ToolboxCommandKind.ArchiveDesktop,
Arguments = new Dictionary<string, string> { ["profile"] = args[2] }
});
Console.WriteLine(result.Summary);
return result.Success ? 0 : 1;
}
Console.Error.WriteLine("用法: WindowsToolbox.Cli desktop-archive --profile <id>");
return 2;
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\WindowsToolbox.Core\WindowsToolbox.Core.csproj" />
<ProjectReference Include="..\WindowsToolbox.Infrastructure\WindowsToolbox.Infrastructure.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+162
View File
@@ -0,0 +1,162 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace WindowsToolbox.Core;
public enum ToolboxCommandKind
{
DetectStatus,
EnableClassicContextMenu,
RestoreContextMenu,
HideShortcutArrow,
RestoreShortcutArrow,
PauseWindowsUpdate,
DisableWindowsUpdate,
RestoreWindowsUpdate,
ConfigureUpdateRenewal,
DisableDefenderProtection,
RestoreDefenderProtection,
PrepareAutoLogon,
RestoreAutoLogonPreparation,
ArchiveDesktop,
UndoDesktopArchive,
ConfigureDesktopSchedule,
ActivateWindowsKms,
RestoreKmsConfiguration
}
public sealed record ToolboxCommand
{
public required string Id { get; init; }
public required ToolboxCommandKind Kind { get; init; }
public Dictionary<string, string> Arguments { get; init; } = new(StringComparer.OrdinalIgnoreCase);
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.Now;
public string? ResultPath { get; init; }
}
public sealed record CommandStepResult(
string Name,
bool Success,
string Message,
bool RequiresRestart = false);
public sealed record CommandResult
{
public required string CommandId { get; init; }
public required ToolboxCommandKind Kind { get; init; }
public bool Success { get; init; }
public string Summary { get; init; } = string.Empty;
public List<CommandStepResult> Steps { get; init; } = [];
public DateTimeOffset CompletedAt { get; init; } = DateTimeOffset.Now;
}
public sealed record SystemStatus
{
public string ProductName { get; init; } = "Windows";
public string EditionId { get; init; } = "Unknown";
public string DisplayVersion { get; init; } = "Unknown";
public string Build { get; init; } = "Unknown";
public bool IsWindows11 { get; init; }
public bool IsAdministrator { get; init; }
public bool ClassicContextMenuEnabled { get; init; }
public bool ShortcutArrowHidden { get; init; }
public string UpdateMode { get; init; } = "检测中";
public DateTimeOffset? UpdatePauseUntil { get; init; }
public bool? DefenderRealtimeProtectionEnabled { get; init; }
public bool? TamperProtectionEnabled { get; init; }
public bool AutoLogonOptionPrepared { get; init; }
public string ActivationStatus { get; init; } = "未知";
public string? PartialProductKey { get; init; }
public string? KmsHost { get; init; }
}
public sealed record RegistryValueSnapshot
{
public required string Hive { get; init; }
public required string KeyPath { get; init; }
public required string ValueName { get; init; }
public bool Exists { get; init; }
public int Kind { get; init; }
public string? TextValue { get; init; }
public string[]? TextValues { get; init; }
public byte[]? BinaryValue { get; init; }
public long? NumericValue { get; init; }
}
public sealed record FeatureSnapshot
{
public required string Id { get; init; }
public required string Feature { get; init; }
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.Now;
public List<RegistryValueSnapshot> RegistryValues { get; init; } = [];
public Dictionary<string, string?> Metadata { get; init; } = new(StringComparer.OrdinalIgnoreCase);
}
public sealed record DesktopArchiveProfile
{
public string Id { get; init; } = "default";
public required string DestinationRoot { get; init; }
public bool ExcludeShortcuts { get; init; } = true;
public bool RenameOnCollision { get; init; } = true;
public int MonthlyDay { get; init; } = 1;
public TimeOnly RunAt { get; init; } = new(9, 0);
}
public sealed record ArchivedFile(string OriginalPath, string ArchivedPath, long Length, bool IsDirectory = false);
public sealed record ArchiveManifest
{
public required string Id { get; init; }
public required string ProfileId { get; init; }
public required string DestinationFolder { get; init; }
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.Now;
public List<ArchivedFile> Files { get; init; } = [];
public List<string> Skipped { get; init; } = [];
public List<string> Errors { get; init; } = [];
}
public sealed record KmsKeyEntry(
string EditionId,
string DisplayName,
string ProductKey,
string AppliesTo,
bool Supported = true);
public sealed record OperationLogEntry
{
public required string Id { get; init; }
public required string Feature { get; init; }
public required string Action { get; init; }
public required bool Success { get; init; }
public required string Summary { get; init; }
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.Now;
public List<CommandStepResult> Steps { get; init; } = [];
}
public static class JsonDefaults
{
public static readonly JsonSerializerOptions Options = new()
{
WriteIndented = true,
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter() }
};
}
public static class ToolboxPaths
{
public static string UserRoot => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"WindowsToolbox");
public static string MachineRoot => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"WindowsToolbox");
public static string PendingCommands => Path.Combine(UserRoot, "Pending");
public static string Profiles => Path.Combine(UserRoot, "Profiles");
public static string Manifests => Path.Combine(UserRoot, "ArchiveManifests");
public static string Logs => Path.Combine(UserRoot, "Logs");
public static string Snapshots => Path.Combine(MachineRoot, "Snapshots");
public static string Assets => Path.Combine(MachineRoot, "Assets");
}
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,74 @@
using WindowsToolbox.Core;
using WindowsToolbox.Infrastructure;
try
{
if (!SystemStatusService.IsAdministrator())
{
Console.Error.WriteLine("WindowsToolbox.ElevatedHost 必须以管理员身份运行。");
return 5;
}
var dispatcher = new ToolboxCommandDispatcher();
ToolboxCommand command;
if (args.Length == 2 && args[0].Equals("--command-file", StringComparison.OrdinalIgnoreCase))
{
var path = ValidatePendingPath(args[1]);
command = await JsonFileStore.LoadAsync<ToolboxCommand>(path)
?? throw new InvalidDataException("命令文件为空或格式无效");
if (DateTimeOffset.Now - command.CreatedAt > TimeSpan.FromMinutes(10))
{
throw new InvalidOperationException("命令文件已过期");
}
if (!string.IsNullOrWhiteSpace(command.ResultPath))
{
_ = ValidatePendingPath(command.ResultPath);
}
}
else if (args.Length == 1 && args[0].Equals("--scheduled-update-disable", StringComparison.OrdinalIgnoreCase))
{
command = new ToolboxCommand
{
Id = Guid.NewGuid().ToString("N"),
Kind = ToolboxCommandKind.DisableWindowsUpdate,
Arguments = new Dictionary<string, string> { ["guard"] = "true" }
};
}
else if (args.Length == 2 && args[0].Equals("--scheduled-update-renew", StringComparison.OrdinalIgnoreCase))
{
command = new ToolboxCommand
{
Id = Guid.NewGuid().ToString("N"),
Kind = ToolboxCommandKind.PauseWindowsUpdate,
Arguments = new Dictionary<string, string> { ["days"] = args[1] }
};
}
else
{
Console.Error.WriteLine("用法: --command-file <path> | --scheduled-update-disable | --scheduled-update-renew <days>");
return 2;
}
var result = await dispatcher.ExecuteAsync(command);
Console.WriteLine(result.Summary);
return result.Success ? 0 : 1;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
return 1;
}
static string ValidatePendingPath(string path)
{
var root = Path.GetFullPath(ToolboxPaths.PendingCommands).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
var fullPath = Path.GetFullPath(path);
if (!fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase))
{
throw new UnauthorizedAccessException("命令文件必须位于 WindowsToolbox Pending 目录中");
}
return fullPath;
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\WindowsToolbox.Core\WindowsToolbox.Core.csproj" />
<ProjectReference Include="..\WindowsToolbox.Infrastructure\WindowsToolbox.Infrastructure.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\data\kms-keys.json" Link="data\kms-keys.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="WindowsToolbox.ElevatedHost" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{4f476546-937d-4f42-b2d6-a2111a7e8f8d}" />
</application>
</compatibility>
</assembly>
@@ -0,0 +1,231 @@
using System.Net.Sockets;
using System.Text.Json;
using System.Text.RegularExpressions;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed partial class ActivationService(
RegistryStore registry,
SnapshotStore snapshots,
ProcessRunner processes)
{
private const string Feature = "kms-configuration";
private const string SppKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform";
public IReadOnlyList<KmsKeyEntry> LoadCatalog()
{
var candidates = new[]
{
Path.Combine(AppContext.BaseDirectory, "data", "kms-keys.json"),
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "data", "kms-keys.json"))
};
foreach (var path in candidates)
{
try
{
if (!File.Exists(path))
{
continue;
}
return JsonSerializer.Deserialize<List<KmsKeyEntry>>(File.ReadAllText(path), JsonDefaults.Options) ?? [];
}
catch
{
// Continue to the next catalog location.
}
}
return [];
}
public async Task<CommandResult> ActivateAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
var steps = new List<CommandStepResult>();
try
{
var edition = command.Arguments.GetValueOrDefault("edition") ?? throw new ArgumentException("请选择 Windows 版本");
var host = command.Arguments.GetValueOrDefault("host")?.Trim() ?? throw new ArgumentException("请输入本地 KMS 主机地址");
var port = int.TryParse(command.Arguments.GetValueOrDefault("port"), out var parsedPort) ? parsedPort : 1688;
ValidateHost(host, port);
var entry = LoadCatalog().FirstOrDefault(x => x.EditionId.Equals(edition, StringComparison.OrdinalIgnoreCase))
?? throw new ArgumentException("密钥目录中没有所选版本");
if (!entry.Supported || !ProductKeyRegex().IsMatch(entry.ProductKey))
{
throw new InvalidOperationException("所选 Windows 版本不支持 KMS 客户端激活");
}
var reachable = await TestPortAsync(host, port, cancellationToken);
steps.Add(new CommandStepResult("KMS 连接", reachable,
reachable ? $"已连接 {host}:{port}" : $"无法连接 {host}:{port},没有更改产品密钥"));
if (!reachable)
{
return Build(command, steps, "本地 KMS 服务器不可达");
}
await snapshots.GetOrCreateAsync(Feature, () => new FeatureSnapshot
{
Id = Guid.NewGuid().ToString("N"),
Feature = Feature,
RegistryValues =
[
registry.Capture("HKLM", SppKey, "KeyManagementServiceName"),
registry.Capture("HKLM", SppKey, "KeyManagementServicePort")
],
Metadata = { ["PreviousKeyRecoverable"] = "False" }
}, cancellationToken);
var install = await RunSlmgrAsync("/ipk", entry.ProductKey, cancellationToken);
steps.Add(new CommandStepResult("安装 GVLK", install.Success,
install.Success ? $"已安装 {entry.DisplayName} 客户端密钥" : Friendly(install, "安装密钥失败")));
if (!install.Success)
{
return Build(command, steps, "KMS 客户端密钥安装失败");
}
var endpoint = FormatEndpoint(host, port);
var setHost = await RunSlmgrAsync("/skms", endpoint, cancellationToken);
steps.Add(new CommandStepResult("设置 KMS 主机", setHost.Success,
setHost.Success ? $"已设置 {endpoint}" : Friendly(setHost, "设置 KMS 主机失败")));
if (!setHost.Success)
{
return Build(command, steps, "KMS 主机设置失败");
}
var activate = await RunSlmgrAsync("/ato", null, cancellationToken);
steps.Add(new CommandStepResult("执行激活", activate.Success,
Friendly(activate, activate.Success ? "激活命令已完成" : "激活失败")));
var verify = await processes.RunPowerShellAsync(
"$p=Get-CimInstance SoftwareLicensingProduct -Filter \"Name like 'Windows%'\" | " +
"Where-Object {$_.PartialProductKey} | Select-Object -First 1; [int]$p.LicenseStatus",
cancellationToken);
var licensed = verify.Success && verify.StandardOutput
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Any(line => line == "1");
steps.Add(new CommandStepResult("激活状态验证", licensed,
licensed ? "Windows 报告许可证状态为已激活" : "激活命令结束,但 Windows 尚未报告已激活状态"));
return Build(command, steps, licensed ? "Windows KMS 激活成功" : "Windows KMS 激活未通过状态验证");
}
catch (Exception ex)
{
steps.Add(new CommandStepResult("KMS 激活", false, ex.Message));
return Build(command, steps, "KMS 激活失败");
}
}
public async Task<CommandResult> RestoreAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
var steps = new List<CommandStepResult>();
try
{
var snapshot = await snapshots.LoadAsync(Feature, cancellationToken);
if (snapshot is null)
{
var clear = await RunSlmgrAsync("/ckms", null, cancellationToken);
steps.Add(new CommandStepResult("清除 KMS 主机", clear.Success, Friendly(clear, "已请求清除 KMS 主机")));
return Build(command, steps, "没有基线快照;已清除 KMS 主机配置");
}
var hostValue = snapshot.RegistryValues.First(x => x.ValueName == "KeyManagementServiceName");
var portValue = snapshot.RegistryValues.First(x => x.ValueName == "KeyManagementServicePort");
ProcessResult result;
if (hostValue.Exists && !string.IsNullOrWhiteSpace(hostValue.TextValue))
{
var oldPort = portValue.NumericValue?.ToString() ?? portValue.TextValue ?? "1688";
result = await RunSlmgrAsync("/skms", FormatEndpoint(hostValue.TextValue, int.TryParse(oldPort, out var parsed) ? parsed : 1688), cancellationToken);
}
else
{
result = await RunSlmgrAsync("/ckms", null, cancellationToken);
}
steps.Add(new CommandStepResult("恢复 KMS 主机", result.Success,
Friendly(result, result.Success ? "KMS 主机配置已恢复" : "KMS 主机配置恢复失败")));
foreach (var value in snapshot.RegistryValues)
{
registry.Restore(value);
}
steps.Add(new CommandStepResult("产品密钥", true,
"KMS 主机已恢复;Windows 不提供原完整产品密钥,因此产品密钥本身不会自动还原"));
if (result.Success)
{
snapshots.Delete(Feature);
}
return Build(command, steps, result.Success ? "KMS 主机配置已恢复" : "KMS 配置恢复失败");
}
catch (Exception ex)
{
steps.Add(new CommandStepResult("恢复 KMS", false, ex.Message));
return Build(command, steps, "恢复 KMS 配置失败");
}
}
private Task<ProcessResult> RunSlmgrAsync(string option, string? value, CancellationToken cancellationToken)
{
var script = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "slmgr.vbs");
var args = new List<string> { "//NoLogo", script, option };
if (!string.IsNullOrWhiteSpace(value))
{
args.Add(value);
}
return processes.RunAsync("cscript.exe", args, cancellationToken);
}
private static async Task<bool> TestPortAsync(string host, int port, CancellationToken cancellationToken)
{
try
{
using var client = new TcpClient();
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(4));
await client.ConnectAsync(host, port, timeout.Token);
return true;
}
catch
{
return false;
}
}
private static void ValidateHost(string host, int port)
{
if (host.Length is < 1 or > 255 || !HostRegex().IsMatch(host))
{
throw new ArgumentException("KMS 主机只能是 IP 地址或由字母、数字、点、冒号和连字符组成的主机名");
}
if (port is < 1 or > 65535)
{
throw new ArgumentOutOfRangeException(nameof(port), "KMS 端口必须在 1 到 65535 之间");
}
}
private static string FormatEndpoint(string host, int port) =>
host.StartsWith("[", StringComparison.Ordinal) && host.EndsWith("]", StringComparison.Ordinal)
? $"{host}:{port}"
: host.Contains(':', StringComparison.Ordinal)
? $"[{host}]:{port}"
: $"{host}:{port}";
private static CommandResult Build(ToolboxCommand command, List<CommandStepResult> steps, string summary) => new()
{
CommandId = command.Id,
Kind = command.Kind,
Success = steps.Count > 0 && steps.All(x => x.Success),
Summary = summary,
Steps = steps
};
private static string Friendly(ProcessResult result, string fallback) =>
string.IsNullOrWhiteSpace(result.CombinedOutput) ? fallback : result.CombinedOutput.Trim();
[GeneratedRegex(@"^[A-Za-z0-9.:-]{1,255}$", RegexOptions.CultureInvariant)]
private static partial Regex HostRegex();
[GeneratedRegex(@"^[A-Z0-9]{5}(?:-[A-Z0-9]{5}){4}$", RegexOptions.CultureInvariant)]
private static partial Regex ProductKeyRegex();
}
@@ -0,0 +1,176 @@
using Microsoft.Win32;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class AppearanceService(RegistryStore registry, SnapshotStore snapshots)
{
private const string ClassicFeature = "classic-context-menu";
private const string ClassicKey = @"Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}";
private const string ClassicServerKey = ClassicKey + @"\InprocServer32";
private const string AtlasRootKey = @"SOFTWARE\AtlasOS";
private const string AtlasContextMenuKey = @"SOFTWARE\AtlasOS\Services\OldContextMenu";
private const string ArrowFeature = "shortcut-arrow";
private const string ShellIconsKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Icons";
public async Task<CommandResult> EnableClassicContextMenuAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
try
{
await snapshots.GetOrCreateAsync(ClassicFeature, () =>
{
var atlasPresent = registry.KeyExists("HKLM", AtlasRootKey);
var snapshot = new FeatureSnapshot
{
Id = Guid.NewGuid().ToString("N"),
Feature = ClassicFeature,
RegistryValues = [registry.Capture("HKCU", ClassicServerKey, string.Empty)],
Metadata =
{
["RootKeyExisted"] = registry.KeyExists("HKCU", ClassicKey).ToString(),
["AtlasPresent"] = atlasPresent.ToString()
}
};
if (atlasPresent)
{
snapshot.RegistryValues.Add(registry.Capture("HKLM", AtlasContextMenuKey, "state"));
}
return snapshot;
}, cancellationToken);
registry.Set("HKCU", ClassicServerKey, string.Empty, string.Empty, RegistryValueKind.String);
if (registry.KeyExists("HKLM", AtlasRootKey))
{
registry.Set("HKLM", AtlasContextMenuKey, "state", 1, RegistryValueKind.DWord);
}
var verified = registry.KeyExists("HKCU", ClassicServerKey);
return Result(command, verified, verified ? "经典右键菜单已启用" : "注册表写入后验证失败", true);
}
catch (Exception ex)
{
return Result(command, false, ex.Message);
}
}
public async Task<CommandResult> RestoreContextMenuAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
try
{
var snapshot = await snapshots.LoadAsync(ClassicFeature, cancellationToken);
if (snapshot is null)
{
registry.DeleteKeyTree("HKCU", ClassicKey);
if (registry.KeyExists("HKLM", AtlasRootKey))
{
registry.Set("HKLM", AtlasContextMenuKey, "state", 0, RegistryValueKind.DWord);
}
}
else if (snapshot.Metadata.TryGetValue("RootKeyExisted", out var existed) &&
bool.TryParse(existed, out var rootExisted) && !rootExisted)
{
registry.DeleteKeyTree("HKCU", ClassicKey);
foreach (var value in snapshot.RegistryValues.Where(value => value.Hive.Equals("HKLM", StringComparison.OrdinalIgnoreCase)))
{
registry.Restore(value);
}
}
else
{
foreach (var value in snapshot.RegistryValues)
{
registry.Restore(value);
}
}
snapshots.Delete(ClassicFeature);
return Result(command, true, "右键菜单设置已恢复", true);
}
catch (Exception ex)
{
return Result(command, false, ex.Message);
}
}
public async Task<CommandResult> HideShortcutArrowAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
try
{
await snapshots.GetOrCreateAsync(ArrowFeature, () => new FeatureSnapshot
{
Id = Guid.NewGuid().ToString("N"),
Feature = ArrowFeature,
RegistryValues = [registry.Capture("HKLM", ShellIconsKey, "29")]
}, cancellationToken);
Directory.CreateDirectory(ToolboxPaths.Assets);
var iconPath = Path.Combine(ToolboxPaths.Assets, "blank.ico");
if (!File.Exists(iconPath))
{
File.WriteAllBytes(iconPath, CreateTransparentIcon());
}
registry.Set("HKLM", ShellIconsKey, "29", $"{iconPath},0", RegistryValueKind.String);
var actual = registry.Read("HKLM", ShellIconsKey, "29")?.ToString();
var verified = string.Equals(actual, $"{iconPath},0", StringComparison.OrdinalIgnoreCase);
return Result(command, verified, verified ? "快捷方式箭头已隐藏" : "图标覆盖写入后验证失败", true);
}
catch (Exception ex)
{
return Result(command, false, ex.Message);
}
}
public async Task<CommandResult> RestoreShortcutArrowAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
try
{
var snapshot = await snapshots.LoadAsync(ArrowFeature, cancellationToken);
if (snapshot is null)
{
registry.DeleteValue("HKLM", ShellIconsKey, "29");
}
else
{
foreach (var value in snapshot.RegistryValues)
{
registry.Restore(value);
}
}
snapshots.Delete(ArrowFeature);
return Result(command, true, "快捷方式箭头设置已恢复", true);
}
catch (Exception ex)
{
return Result(command, false, ex.Message);
}
}
private static CommandResult Result(ToolboxCommand command, bool success, string message, bool restart = false) => new()
{
CommandId = command.Id,
Kind = command.Kind,
Success = success,
Summary = message,
Steps = [new CommandStepResult("外观设置", success, message, restart)]
};
private static byte[] CreateTransparentIcon() =>
[
0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x20, 0x00,
0x30, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00,
0x28, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00,
0x01, 0x00, 0x20, 0x00,
0x00, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0xFF, 0x00, 0x00, 0x00
];
}
@@ -0,0 +1,71 @@
using System.Diagnostics;
using Microsoft.Win32;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class AutoLogonService(RegistryStore registry, SnapshotStore snapshots)
{
private const string Feature = "auto-logon-option";
private const string Key = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device";
private const string ValueName = "DevicePasswordLessBuildVersion";
public async Task<CommandResult> PrepareAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
try
{
await snapshots.GetOrCreateAsync(Feature, () => new FeatureSnapshot
{
Id = Guid.NewGuid().ToString("N"),
Feature = Feature,
RegistryValues = [registry.Capture("HKLM", Key, ValueName)]
}, cancellationToken);
registry.Set("HKLM", Key, ValueName, 0, RegistryValueKind.DWord);
var prepared = Convert.ToInt32(registry.Read("HKLM", Key, ValueName) ?? 1) == 0;
return Build(command, prepared, prepared
? "自动登录选项已准备;请在随后打开的账户窗口中完成设置"
: "自动登录选项注册表验证失败");
}
catch (Exception ex)
{
return Build(command, false, ex.Message);
}
}
public async Task<CommandResult> RestoreAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
try
{
var snapshot = await snapshots.LoadAsync(Feature, cancellationToken);
if (snapshot is null)
{
registry.DeleteValue("HKLM", Key, ValueName);
}
else
{
foreach (var value in snapshot.RegistryValues)
{
registry.Restore(value);
}
}
snapshots.Delete(Feature);
return Build(command, true, "自动登录选项准备设置已恢复;账户本身的自动登录设置请在 netplwiz 中修改");
}
catch (Exception ex)
{
return Build(command, false, ex.Message);
}
}
public static void OpenNetplwiz() => Process.Start(new ProcessStartInfo("netplwiz.exe") { UseShellExecute = true });
private static CommandResult Build(ToolboxCommand command, bool success, string summary) => new()
{
CommandId = command.Id,
Kind = command.Kind,
Success = success,
Summary = summary,
Steps = [new CommandStepResult("自动登录", success, summary)]
};
}
@@ -0,0 +1,32 @@
namespace WindowsToolbox.Infrastructure;
public static class CompanionLocator
{
public static string Find(string fileName)
{
var adjacent = Path.Combine(AppContext.BaseDirectory, fileName);
if (File.Exists(adjacent))
{
return adjacent;
}
var directory = new DirectoryInfo(AppContext.BaseDirectory);
for (var i = 0; i < 7 && directory is not null; i++, directory = directory.Parent)
{
var candidates = Directory.Exists(directory.FullName)
? Directory.EnumerateFiles(directory.FullName, fileName, SearchOption.AllDirectories)
.Where(path => !path.Contains($"{Path.DirectorySeparatorChar}.tools{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
.OrderByDescending(File.GetLastWriteTimeUtc)
.Take(1)
.ToArray()
: [];
if (candidates.Length > 0)
{
return candidates[0];
}
}
throw new FileNotFoundException($"没有找到配套程序 {fileName}。请重新构建或安装完整工具箱。", fileName);
}
}
@@ -0,0 +1,361 @@
using System.Runtime.InteropServices;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class DesktopArchiveService
{
private readonly TaskSchedulerService _taskScheduler;
private readonly Func<string> _desktopPathProvider;
private readonly string _profilesRoot;
private readonly string _manifestsRoot;
private static readonly HashSet<string> ExcludedExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".lnk", ".url"
};
public DesktopArchiveService(
TaskSchedulerService taskScheduler,
Func<string>? desktopPathProvider = null,
string? dataRoot = null)
{
_taskScheduler = taskScheduler;
_desktopPathProvider = desktopPathProvider ?? (() => KnownFolderPath(FolderIds.Desktop));
_profilesRoot = dataRoot is null ? ToolboxPaths.Profiles : Path.Combine(dataRoot, "Profiles");
_manifestsRoot = dataRoot is null ? ToolboxPaths.Manifests : Path.Combine(dataRoot, "ArchiveManifests");
}
public string GetDesktopPath() => _desktopPathProvider();
public async Task SaveProfileAsync(DesktopArchiveProfile profile, CancellationToken cancellationToken = default)
{
ValidateProfile(profile);
await JsonFileStore.SaveAsync(ProfilePath(profile.Id), profile, cancellationToken);
}
public Task<DesktopArchiveProfile?> LoadProfileAsync(string id, CancellationToken cancellationToken = default) =>
JsonFileStore.LoadAsync<DesktopArchiveProfile>(ProfilePath(id), cancellationToken);
public Task<CommandResult> ArchiveAsync(ToolboxCommand command, CancellationToken cancellationToken = default) =>
Task.Run(() => ArchiveCoreAsync(command, cancellationToken), cancellationToken);
private async Task<CommandResult> ArchiveCoreAsync(ToolboxCommand command, CancellationToken cancellationToken)
{
try
{
var profile = await ResolveProfileAsync(command, cancellationToken);
ValidateProfile(profile);
var desktop = Path.GetFullPath(GetDesktopPath());
var destinationRoot = Path.GetFullPath(profile.DestinationRoot);
if (IsSameOrChildPath(destinationRoot, desktop))
{
throw new InvalidOperationException("归档目标不能位于桌面目录内部,否则可能把归档目录再次移动");
}
var destination = Path.Combine(destinationRoot, DateTime.Now.ToString("yyyy-MM-dd"));
Directory.CreateDirectory(destination);
var manifest = new ArchiveManifest
{
Id = Guid.NewGuid().ToString("N"),
ProfileId = profile.Id,
DestinationFolder = destination
};
var manifestPath = Path.Combine(_manifestsRoot, $"{manifest.CreatedAt:yyyyMMdd-HHmmssfffffff}-{manifest.Id}.json");
await JsonFileStore.SaveAsync(manifestPath, manifest, cancellationToken);
foreach (var source in Directory.EnumerateFileSystemEntries(desktop, "*", SearchOption.TopDirectoryOnly).ToArray())
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var name = Path.GetFileName(source);
var attributes = File.GetAttributes(source);
var isDirectory = attributes.HasFlag(FileAttributes.Directory);
if (name.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase) ||
!isDirectory && profile.ExcludeShortcuts && ExcludedExtensions.Contains(Path.GetExtension(source)) ||
attributes.HasFlag(FileAttributes.System) ||
isDirectory && attributes.HasFlag(FileAttributes.ReparsePoint))
{
manifest.Skipped.Add(source);
continue;
}
var target = Path.Combine(destination, name);
if (File.Exists(target) || Directory.Exists(target))
{
if (!profile.RenameOnCollision)
{
manifest.Skipped.Add(source);
continue;
}
target = GetUniquePath(target, isDirectory);
}
var length = isDirectory ? 0 : new FileInfo(source).Length;
if (isDirectory)
{
MoveDirectory(source, target);
}
else
{
File.Move(source, target);
}
manifest.Files.Add(new ArchivedFile(source, target, length, isDirectory));
await JsonFileStore.SaveAsync(manifestPath, manifest, cancellationToken);
}
catch (Exception ex)
{
manifest.Errors.Add($"{source}: {ex.Message}");
}
}
await JsonFileStore.SaveAsync(manifestPath, manifest, cancellationToken);
var success = manifest.Errors.Count == 0;
var summary = $"已移动 {manifest.Files.Count} 个项目,跳过 {manifest.Skipped.Count} 个,失败 {manifest.Errors.Count} 个";
return new CommandResult
{
CommandId = command.Id,
Kind = command.Kind,
Success = success,
Summary = summary,
Steps =
[
new CommandStepResult("桌面归档", success, summary),
new CommandStepResult("恢复清单", true, manifestPath)
]
};
}
catch (Exception ex)
{
return Failure(command, ex.Message);
}
}
public Task<CommandResult> UndoLatestAsync(ToolboxCommand command, CancellationToken cancellationToken = default) =>
Task.Run(() => UndoLatestCoreAsync(command, cancellationToken), cancellationToken);
private async Task<CommandResult> UndoLatestCoreAsync(ToolboxCommand command, CancellationToken cancellationToken)
{
try
{
if (!Directory.Exists(_manifestsRoot))
{
return Failure(command, "没有可恢复的桌面归档记录");
}
var path = Directory.EnumerateFiles(_manifestsRoot, "*.json")
.OrderByDescending(Path.GetFileName)
.FirstOrDefault();
if (path is null)
{
return Failure(command, "没有可恢复的桌面归档记录");
}
var manifest = await JsonFileStore.LoadAsync<ArchiveManifest>(path, cancellationToken)
?? throw new InvalidDataException("归档恢复清单损坏");
var restored = 0;
var errors = new List<string>();
foreach (var item in manifest.Files)
{
try
{
var exists = item.IsDirectory ? Directory.Exists(item.ArchivedPath) : File.Exists(item.ArchivedPath);
if (!exists)
{
continue;
}
var targetDirectory = Path.GetDirectoryName(item.OriginalPath)!;
Directory.CreateDirectory(targetDirectory);
var occupied = File.Exists(item.OriginalPath) || Directory.Exists(item.OriginalPath);
var target = occupied ? GetUniquePath(item.OriginalPath, item.IsDirectory) : item.OriginalPath;
if (item.IsDirectory)
{
MoveDirectory(item.ArchivedPath, target);
}
else
{
File.Move(item.ArchivedPath, target);
}
restored++;
}
catch (Exception ex)
{
errors.Add($"{item.ArchivedPath}: {ex.Message}");
}
}
if (errors.Count == 0)
{
File.Delete(path);
}
var summary = $"已恢复 {restored} 个项目,失败 {errors.Count} 个";
return new CommandResult
{
CommandId = command.Id,
Kind = command.Kind,
Success = errors.Count == 0,
Summary = summary,
Steps = [new CommandStepResult("恢复桌面文件", errors.Count == 0, summary)]
};
}
catch (Exception ex)
{
return Failure(command, ex.Message);
}
}
public async Task<CommandResult> ConfigureScheduleAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
try
{
var enabled = command.Arguments.TryGetValue("enabled", out var value) && bool.Parse(value);
var profile = await ResolveProfileAsync(command, cancellationToken);
await SaveProfileAsync(profile, cancellationToken);
var taskName = $"DesktopArchive-{profile.Id}";
if (enabled)
{
var cli = CompanionLocator.Find("WindowsToolbox.Cli.exe");
_taskScheduler.RegisterMonthlyTask(taskName, cli, $"desktop-archive --profile {profile.Id}", profile.MonthlyDay, profile.RunAt);
}
else
{
_taskScheduler.DeleteToolboxTask(taskName);
}
var summary = enabled ? "桌面归档计划任务已创建" : "桌面归档计划任务已删除";
return new CommandResult
{
CommandId = command.Id,
Kind = command.Kind,
Success = true,
Summary = summary,
Steps = [new CommandStepResult("计划任务", true, summary)]
};
}
catch (Exception ex)
{
return Failure(command, ex.Message);
}
}
private static void ValidateProfile(DesktopArchiveProfile profile)
{
if (string.IsNullOrWhiteSpace(profile.DestinationRoot))
{
throw new ArgumentException("请选择归档目标文件夹");
}
if (profile.MonthlyDay is < 1 or > 28)
{
throw new ArgumentOutOfRangeException(nameof(profile.MonthlyDay), "每月执行日期必须在 1 到 28 之间");
}
}
private async Task<DesktopArchiveProfile> ResolveProfileAsync(ToolboxCommand command, CancellationToken cancellationToken)
{
var profileId = command.Arguments.GetValueOrDefault("profile", "default");
var saved = await JsonFileStore.LoadAsync<DesktopArchiveProfile>(ProfilePath(profileId), cancellationToken);
if (saved is not null && !command.Arguments.ContainsKey("destination"))
{
return saved;
}
var destination = command.Arguments.GetValueOrDefault("destination")
?? throw new ArgumentException("缺少桌面归档目标文件夹");
var day = int.TryParse(command.Arguments.GetValueOrDefault("day"), out var parsedDay) ? parsedDay : 1;
var runAt = TimeOnly.TryParse(command.Arguments.GetValueOrDefault("time"), out var parsedTime)
? parsedTime
: new TimeOnly(9, 0);
return new DesktopArchiveProfile
{
Id = profileId,
DestinationRoot = destination,
MonthlyDay = day,
RunAt = runAt
};
}
private string ProfilePath(string id) => Path.Combine(_profilesRoot, $"desktop-{id}.json");
private static string GetUniquePath(string path, bool isDirectory = false)
{
var directory = Path.GetDirectoryName(path)!;
var name = isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
var extension = isDirectory ? string.Empty : Path.GetExtension(path);
for (var index = 1; index < int.MaxValue; index++)
{
var candidate = Path.Combine(directory, $"{name} ({index}){extension}");
if (!File.Exists(candidate) && !Directory.Exists(candidate))
{
return candidate;
}
}
throw new IOException($"无法为 {path} 生成不冲突的文件名");
}
private static void MoveDirectory(string source, string target)
{
var sourceRoot = Path.GetPathRoot(Path.GetFullPath(source));
var targetRoot = Path.GetPathRoot(Path.GetFullPath(target));
if (string.Equals(sourceRoot, targetRoot, StringComparison.OrdinalIgnoreCase))
{
Directory.Move(source, target);
return;
}
Microsoft.VisualBasic.FileIO.FileSystem.MoveDirectory(source, target);
}
private static bool IsSameOrChildPath(string candidate, string parent)
{
var normalizedCandidate = Path.GetFullPath(candidate).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var normalizedParent = Path.GetFullPath(parent).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return normalizedCandidate.Equals(normalizedParent, StringComparison.OrdinalIgnoreCase) ||
normalizedCandidate.StartsWith(normalizedParent + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}
private static CommandResult Failure(ToolboxCommand command, string message) => new()
{
CommandId = command.Id,
Kind = command.Kind,
Success = false,
Summary = message,
Steps = [new CommandStepResult("桌面归档", false, message)]
};
private static string KnownFolderPath(Guid id)
{
var result = SHGetKnownFolderPath(id, 0, IntPtr.Zero, out var pointer);
if (result != 0)
{
Marshal.ThrowExceptionForHR(result);
}
try
{
return Marshal.PtrToStringUni(pointer)
?? Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
}
finally
{
Marshal.FreeCoTaskMem(pointer);
}
}
[DllImport("shell32.dll")]
private static extern int SHGetKnownFolderPath(
[MarshalAs(UnmanagedType.LPStruct)] Guid rfid,
uint flags,
IntPtr token,
out IntPtr path);
private static class FolderIds
{
public static readonly Guid Desktop = new("B4BFCC3A-DB2C-424C-B029-7FE99A87C641");
}
}
@@ -0,0 +1,98 @@
using System.ComponentModel;
using System.Diagnostics;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class ElevatedCommandClient
{
private readonly ToolboxCommandDispatcher _dispatcher = new();
public async Task<CommandResult> ExecuteAsync(
ToolboxCommandKind kind,
Dictionary<string, string>? arguments = null,
CancellationToken cancellationToken = default)
{
var id = Guid.NewGuid().ToString("N");
if (!ToolboxCommandDispatcher.RequiresElevation(kind) || SystemStatusService.IsAdministrator())
{
return await _dispatcher.ExecuteAsync(new ToolboxCommand
{
Id = id,
Kind = kind,
Arguments = arguments ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
}, cancellationToken);
}
Directory.CreateDirectory(ToolboxPaths.PendingCommands);
var commandPath = Path.Combine(ToolboxPaths.PendingCommands, $"{id}.command.json");
var resultPath = Path.Combine(ToolboxPaths.PendingCommands, $"{id}.result.json");
var command = new ToolboxCommand
{
Id = id,
Kind = kind,
Arguments = arguments ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
ResultPath = resultPath
};
await JsonFileStore.SaveAsync(commandPath, command, cancellationToken);
try
{
var host = CompanionLocator.Find("WindowsToolbox.ElevatedHost.exe");
using var process = Process.Start(new ProcessStartInfo
{
FileName = host,
UseShellExecute = true,
Verb = "runas",
Arguments = $"--command-file \"{commandPath}\"",
WindowStyle = ProcessWindowStyle.Hidden
});
if (process is null)
{
throw new InvalidOperationException("无法启动管理员执行器");
}
await process.WaitForExitAsync(cancellationToken);
var result = await JsonFileStore.LoadAsync<CommandResult>(resultPath, cancellationToken);
return result ?? new CommandResult
{
CommandId = id,
Kind = kind,
Success = false,
Summary = $"管理员执行器没有返回结果(退出代码 {process.ExitCode}",
Steps = [new CommandStepResult("管理员执行器", false, "没有结果文件")]
};
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
return new CommandResult
{
CommandId = id,
Kind = kind,
Success = false,
Summary = "已取消管理员授权",
Steps = [new CommandStepResult("管理员授权", false, "用户取消了 UAC 提示")]
};
}
finally
{
TryDelete(commandPath);
TryDelete(resultPath);
}
}
private static void TryDelete(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch
{
// Pending files are harmless and overwritten only by unique id.
}
}
}
@@ -0,0 +1,74 @@
using System.Text.Json;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public static class JsonFileStore
{
public static async Task SaveAsync<T>(string path, T value, CancellationToken cancellationToken = default)
{
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(value, JsonDefaults.Options);
var temporaryPath = path + ".tmp";
await File.WriteAllTextAsync(temporaryPath, json, cancellationToken);
File.Move(temporaryPath, path, true);
}
public static async Task<T?> LoadAsync<T>(string path, CancellationToken cancellationToken = default)
{
if (!File.Exists(path))
{
return default;
}
await using var stream = File.OpenRead(path);
return await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options, cancellationToken);
}
}
public sealed class OperationLogStore
{
public async Task AddAsync(OperationLogEntry entry, CancellationToken cancellationToken = default)
{
var path = Path.Combine(ToolboxPaths.Logs, $"{entry.Timestamp:yyyyMMdd-HHmmssfffffff}-{entry.Id}.json");
await JsonFileStore.SaveAsync(path, entry, cancellationToken);
}
public async Task<IReadOnlyList<OperationLogEntry>> ReadRecentAsync(int limit = 100, CancellationToken cancellationToken = default)
{
if (!Directory.Exists(ToolboxPaths.Logs))
{
return [];
}
var entries = new List<OperationLogEntry>();
foreach (var path in Directory.EnumerateFiles(ToolboxPaths.Logs, "*.json")
.OrderByDescending(Path.GetFileName)
.Take(limit))
{
try
{
var entry = await JsonFileStore.LoadAsync<OperationLogEntry>(path, cancellationToken);
if (entry is not null)
{
entries.Add(entry);
}
}
catch (JsonException)
{
// A damaged history entry must not prevent the application from starting.
}
catch (IOException)
{
// A log can be temporarily locked by the elevated host.
}
}
return entries;
}
}
@@ -0,0 +1,67 @@
using System.Diagnostics;
using System.Text;
namespace WindowsToolbox.Infrastructure;
public sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
{
public bool Success => ExitCode == 0;
public string CombinedOutput => string.Join(Environment.NewLine,
new[] { StandardOutput.Trim(), StandardError.Trim() }.Where(x => x.Length > 0));
}
public sealed class ProcessRunner
{
public async Task<ProcessResult> RunAsync(
string fileName,
IEnumerable<string> arguments,
CancellationToken cancellationToken = default)
{
var startInfo = new ProcessStartInfo
{
FileName = fileName,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
foreach (var argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
using var process = new Process { StartInfo = startInfo };
process.Start();
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
try
{
await process.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
try
{
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
await process.WaitForExitAsync(CancellationToken.None);
}
}
catch
{
// Preserve the original cancellation.
}
throw;
}
return new ProcessResult(process.ExitCode, await outputTask, await errorTask);
}
public Task<ProcessResult> RunPowerShellAsync(string script, CancellationToken cancellationToken = default) =>
RunAsync("powershell.exe",
["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
cancellationToken);
}
@@ -0,0 +1,111 @@
using Microsoft.Win32;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class RegistryStore
{
public RegistryValueSnapshot Capture(string hive, string keyPath, string valueName)
{
using var baseKey = OpenBaseKey(hive, writable: false);
using var key = baseKey.OpenSubKey(keyPath, writable: false);
if (key is null || !key.GetValueNames().Contains(valueName, StringComparer.OrdinalIgnoreCase))
{
return new RegistryValueSnapshot
{
Hive = hive,
KeyPath = keyPath,
ValueName = valueName,
Exists = false
};
}
var kind = key.GetValueKind(valueName);
var value = key.GetValue(valueName, null, RegistryValueOptions.DoNotExpandEnvironmentNames);
var snapshot = new RegistryValueSnapshot
{
Hive = hive,
KeyPath = keyPath,
ValueName = valueName,
Exists = true,
Kind = (int)kind
};
return kind switch
{
RegistryValueKind.Binary => snapshot with { BinaryValue = value as byte[] },
RegistryValueKind.MultiString => snapshot with { TextValues = value as string[] },
RegistryValueKind.DWord => snapshot with { NumericValue = Convert.ToInt64(value) },
RegistryValueKind.QWord => snapshot with { NumericValue = Convert.ToInt64(value) },
_ => snapshot with { TextValue = value?.ToString() }
};
}
public bool KeyExists(string hive, string keyPath)
{
using var baseKey = OpenBaseKey(hive, writable: false);
using var key = baseKey.OpenSubKey(keyPath, writable: false);
return key is not null;
}
public object? Read(string hive, string keyPath, string valueName)
{
using var baseKey = OpenBaseKey(hive, writable: false);
using var key = baseKey.OpenSubKey(keyPath, writable: false);
return key?.GetValue(valueName, null, RegistryValueOptions.DoNotExpandEnvironmentNames);
}
public void Set(string hive, string keyPath, string valueName, object value, RegistryValueKind kind)
{
using var baseKey = OpenBaseKey(hive, writable: true);
using var key = baseKey.CreateSubKey(keyPath, writable: true)
?? throw new InvalidOperationException($"无法创建注册表项 {hive}\\{keyPath}");
key.SetValue(valueName, value, kind);
}
public void DeleteValue(string hive, string keyPath, string valueName)
{
using var baseKey = OpenBaseKey(hive, writable: true);
using var key = baseKey.OpenSubKey(keyPath, writable: true);
key?.DeleteValue(valueName, throwOnMissingValue: false);
}
public void DeleteKeyTree(string hive, string keyPath)
{
using var baseKey = OpenBaseKey(hive, writable: true);
baseKey.DeleteSubKeyTree(keyPath, throwOnMissingSubKey: false);
}
public void Restore(RegistryValueSnapshot snapshot)
{
if (!snapshot.Exists)
{
DeleteValue(snapshot.Hive, snapshot.KeyPath, snapshot.ValueName);
return;
}
var kind = (RegistryValueKind)snapshot.Kind;
object value = kind switch
{
RegistryValueKind.Binary => snapshot.BinaryValue ?? [],
RegistryValueKind.MultiString => snapshot.TextValues ?? [],
RegistryValueKind.DWord => checked((int)(snapshot.NumericValue ?? 0)),
RegistryValueKind.QWord => snapshot.NumericValue ?? 0,
_ => snapshot.TextValue ?? string.Empty
};
Set(snapshot.Hive, snapshot.KeyPath, snapshot.ValueName, value, kind);
}
private static RegistryKey OpenBaseKey(string hive, bool writable)
{
var registryHive = hive.ToUpperInvariant() switch
{
"HKCU" or "HKEY_CURRENT_USER" => RegistryHive.CurrentUser,
"HKLM" or "HKEY_LOCAL_MACHINE" => RegistryHive.LocalMachine,
_ => throw new ArgumentOutOfRangeException(nameof(hive), hive, "仅允许 HKCU 和 HKLM")
};
_ = writable;
return RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry64);
}
}
@@ -0,0 +1,55 @@
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class SnapshotStore
{
public string GetPath(string feature) => Path.Combine(ToolboxPaths.Snapshots, $"{Sanitize(feature)}.baseline.json");
public async Task<FeatureSnapshot> GetOrCreateAsync(
string feature,
Func<FeatureSnapshot> factory,
CancellationToken cancellationToken = default)
{
var existing = await LoadAsync(feature, cancellationToken);
if (existing is not null)
{
return existing;
}
var snapshot = factory();
await JsonFileStore.SaveAsync(GetPath(feature), snapshot, cancellationToken);
return snapshot;
}
public async Task<FeatureSnapshot> GetOrCreateAsync(
string feature,
Func<Task<FeatureSnapshot>> factory,
CancellationToken cancellationToken = default)
{
var existing = await LoadAsync(feature, cancellationToken);
if (existing is not null)
{
return existing;
}
var snapshot = await factory();
await JsonFileStore.SaveAsync(GetPath(feature), snapshot, cancellationToken);
return snapshot;
}
public Task<FeatureSnapshot?> LoadAsync(string feature, CancellationToken cancellationToken = default) =>
JsonFileStore.LoadAsync<FeatureSnapshot>(GetPath(feature), cancellationToken);
public void Delete(string feature)
{
var path = GetPath(feature);
if (File.Exists(path))
{
File.Delete(path);
}
}
private static string Sanitize(string value) => string.Concat(value.Select(c =>
char.IsLetterOrDigit(c) || c is '-' or '_' ? c : '_'));
}
@@ -0,0 +1,127 @@
using System.Security.Principal;
using System.Text.Json;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class SystemStatusService(RegistryStore registry, ProcessRunner processes)
{
private const string CurrentVersionKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion";
private const string ClassicKey = @"Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32";
private const string ShellIconsKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Icons";
private const string AuPolicyKey = @"SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU";
private const string UxSettingsKey = @"SOFTWARE\Microsoft\WindowsUpdate\UX\Settings";
private const string AutoLogonKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device";
private const string SppKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform";
public async Task<SystemStatus> DetectAsync(
bool includeAdvanced = true,
SystemStatus? previous = null,
CancellationToken cancellationToken = default)
{
var product = registry.Read("HKLM", CurrentVersionKey, "ProductName")?.ToString() ?? "Windows";
var edition = registry.Read("HKLM", CurrentVersionKey, "EditionID")?.ToString() ?? "Unknown";
var displayVersion = registry.Read("HKLM", CurrentVersionKey, "DisplayVersion")?.ToString()
?? registry.Read("HKLM", CurrentVersionKey, "ReleaseId")?.ToString()
?? "Unknown";
var build = registry.Read("HKLM", CurrentVersionKey, "CurrentBuildNumber")?.ToString() ?? Environment.OSVersion.Version.Build.ToString();
var isWindows11 = int.TryParse(build, out var buildNumber) && buildNumber >= 22000;
if (isWindows11 && product.Contains("Windows 10", StringComparison.OrdinalIgnoreCase))
{
product = product.Replace("Windows 10", "Windows 11", StringComparison.OrdinalIgnoreCase);
}
var noAutoUpdate = Convert.ToInt32(registry.Read("HKLM", AuPolicyKey, "NoAutoUpdate") ?? 0) == 1;
var pauseText = registry.Read("HKLM", UxSettingsKey, "PauseUpdatesExpiryTime")?.ToString();
DateTimeOffset? pauseUntil = DateTimeOffset.TryParse(pauseText, out var parsedPause) ? parsedPause : null;
var updateMode = noAutoUpdate
? "强力关闭"
: pauseUntil > DateTimeOffset.UtcNow
? $"暂停至 {pauseUntil.Value.ToLocalTime():yyyy-MM-dd}"
: "正常";
var defenderEnabled = previous?.DefenderRealtimeProtectionEnabled;
var tamperEnabled = previous?.TamperProtectionEnabled;
var activationStatus = previous?.ActivationStatus ?? "未知";
var partialProductKey = previous?.PartialProductKey;
if (includeAdvanced)
{
try
{
using var advancedTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
advancedTimeout.CancelAfter(TimeSpan.FromSeconds(5));
const string advancedScript =
"$r=[ordered]@{Realtime=$null;Tamper=$null;LicenseStatus=$null;PartialProductKey=$null};" +
"try{$s=Get-MpComputerStatus -ErrorAction Stop;$r.Realtime=$s.RealTimeProtectionEnabled;$r.Tamper=$s.IsTamperProtected}catch{};" +
"try{$p=Get-CimInstance SoftwareLicensingProduct -Filter \"Name like 'Windows%'\" -ErrorAction Stop|" +
"Where-Object {$_.PartialProductKey}|Select-Object -First 1;" +
"if($null-ne$p){$r.LicenseStatus=[int]$p.LicenseStatus;$r.PartialProductKey=$p.PartialProductKey}}catch{};" +
"[pscustomobject]$r|ConvertTo-Json -Compress";
var advanced = await processes.RunPowerShellAsync(advancedScript, advancedTimeout.Token);
if (advanced.Success && !string.IsNullOrWhiteSpace(advanced.StandardOutput))
{
using var document = JsonDocument.Parse(advanced.StandardOutput);
var root = document.RootElement;
if (root.TryGetProperty("Realtime", out var realtime) && realtime.ValueKind is JsonValueKind.True or JsonValueKind.False)
{
defenderEnabled = realtime.GetBoolean();
}
if (root.TryGetProperty("Tamper", out var tamper) && tamper.ValueKind is JsonValueKind.True or JsonValueKind.False)
{
tamperEnabled = tamper.GetBoolean();
}
if (root.TryGetProperty("LicenseStatus", out var license) && license.ValueKind == JsonValueKind.Number)
{
activationStatus = license.GetInt32() switch
{
1 => "已激活",
2 => "初始宽限期",
3 => "额外宽限期",
4 => "非正版宽限期",
5 => "通知模式",
6 => "延长宽限期",
_ => "未激活"
};
}
partialProductKey = root.TryGetProperty("PartialProductKey", out var key) && key.ValueKind == JsonValueKind.String
? key.GetString()
: partialProductKey;
}
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// Advanced status is optional; never hold up the toolbox for more than five seconds.
}
catch (Exception) when (!cancellationToken.IsCancellationRequested)
{
// Preserve basic status if optional Defender or licensing providers are unavailable.
}
}
return new SystemStatus
{
ProductName = product,
EditionId = edition,
DisplayVersion = displayVersion,
Build = build,
IsWindows11 = isWindows11,
IsAdministrator = IsAdministrator(),
ClassicContextMenuEnabled = registry.KeyExists("HKCU", ClassicKey),
ShortcutArrowHidden = registry.Read("HKLM", ShellIconsKey, "29") is not null,
UpdateMode = updateMode,
UpdatePauseUntil = pauseUntil,
DefenderRealtimeProtectionEnabled = defenderEnabled,
TamperProtectionEnabled = tamperEnabled,
AutoLogonOptionPrepared = Convert.ToInt32(registry.Read("HKLM", AutoLogonKey, "DevicePasswordLessBuildVersion") ?? 2) == 0,
ActivationStatus = activationStatus,
PartialProductKey = partialProductKey,
KmsHost = registry.Read("HKLM", SppKey, "KeyManagementServiceName")?.ToString()
};
}
public static bool IsAdministrator()
{
using var identity = WindowsIdentity.GetCurrent();
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
}
}
@@ -0,0 +1,160 @@
using System.Runtime.InteropServices;
namespace WindowsToolbox.Infrastructure;
public sealed class TaskSchedulerService
{
private const int TaskCreateOrUpdate = 6;
private const int TaskLogonInteractiveToken = 3;
private const int TaskActionExec = 0;
private const int TaskTriggerDaily = 2;
private const int TaskTriggerMonthly = 4;
public bool? TryGetEnabled(string taskPath)
{
try
{
var (folderPath, name) = SplitPath(taskPath);
dynamic service = Connect();
dynamic folder = service.GetFolder(folderPath);
dynamic task = folder.GetTask(name);
return (bool)task.Enabled;
}
catch
{
return null;
}
}
public bool TrySetEnabled(string taskPath, bool enabled, out string message)
{
try
{
var (folderPath, name) = SplitPath(taskPath);
dynamic service = Connect();
dynamic folder = service.GetFolder(folderPath);
dynamic task = folder.GetTask(name);
task.Enabled = enabled;
message = enabled ? "已启用" : "已禁用";
return true;
}
catch (Exception ex)
{
message = ex.Message;
return false;
}
}
public void RegisterDailyTask(string name, string executable, string arguments, TimeOnly runAt, bool highestPrivileges)
{
dynamic service = Connect();
dynamic folder = GetOrCreateToolboxFolder(service);
dynamic definition = service.NewTask(0);
ConfigureDefinition(definition, executable, arguments, highestPrivileges);
dynamic trigger = definition.Triggers.Create(TaskTriggerDaily);
var start = DateTime.Today.Add(runAt.ToTimeSpan());
if (start <= DateTime.Now)
{
start = start.AddDays(1);
}
trigger.StartBoundary = start.ToString("s");
trigger.DaysInterval = 1;
Register(folder, name, definition);
}
public void RegisterMonthlyTask(string name, string executable, string arguments, int day, TimeOnly runAt)
{
if (day is < 1 or > 28)
{
throw new ArgumentOutOfRangeException(nameof(day), "每月执行日期必须在 1 到 28 之间");
}
dynamic service = Connect();
dynamic folder = GetOrCreateToolboxFolder(service);
dynamic definition = service.NewTask(0);
ConfigureDefinition(definition, executable, arguments, highestPrivileges: false);
dynamic trigger = definition.Triggers.Create(TaskTriggerMonthly);
var now = DateTime.Now;
var start = new DateTime(now.Year, now.Month, day).Add(runAt.ToTimeSpan());
if (start <= now)
{
start = start.AddMonths(1);
}
trigger.StartBoundary = start.ToString("s");
trigger.DaysOfMonth = 1 << (day - 1);
trigger.MonthsOfYear = 0xFFF;
Register(folder, name, definition);
}
public void DeleteToolboxTask(string name)
{
try
{
dynamic service = Connect();
dynamic folder = GetOrCreateToolboxFolder(service);
folder.DeleteTask(name, 0);
}
catch
{
// Deleting an absent task is idempotent.
}
}
private static void ConfigureDefinition(dynamic definition, string executable, string arguments, bool highestPrivileges)
{
definition.RegistrationInfo.Description = "Windows 工具箱自动任务";
definition.Principal.UserId = $"{Environment.UserDomainName}\\{Environment.UserName}";
definition.Principal.LogonType = TaskLogonInteractiveToken;
definition.Principal.RunLevel = highestPrivileges ? 1 : 0;
definition.Settings.Enabled = true;
definition.Settings.StartWhenAvailable = true;
definition.Settings.DisallowStartIfOnBatteries = false;
definition.Settings.StopIfGoingOnBatteries = false;
definition.Settings.MultipleInstances = 2;
dynamic action = definition.Actions.Create(TaskActionExec);
action.Path = executable;
action.Arguments = arguments;
}
private static void Register(dynamic folder, string name, dynamic definition)
{
folder.RegisterTaskDefinition(
name,
definition,
TaskCreateOrUpdate,
$"{Environment.UserDomainName}\\{Environment.UserName}",
null,
TaskLogonInteractiveToken,
null);
}
private static dynamic GetOrCreateToolboxFolder(dynamic service)
{
dynamic root = service.GetFolder("\\");
try
{
return root.GetFolder("WindowsToolbox");
}
catch
{
return root.CreateFolder("WindowsToolbox");
}
}
private static dynamic Connect()
{
var type = Type.GetTypeFromProgID("Schedule.Service")
?? throw new PlatformNotSupportedException("当前系统没有 Task Scheduler 2.0");
dynamic service = Activator.CreateInstance(type)
?? throw new COMException("无法创建任务计划程序连接");
service.Connect();
return service;
}
private static (string Folder, string Name) SplitPath(string path)
{
var normalized = path.StartsWith('\\') ? path : "\\" + path;
var index = normalized.LastIndexOf('\\');
return (index <= 0 ? "\\" : normalized[..index], normalized[(index + 1)..]);
}
}
@@ -0,0 +1,127 @@
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class ToolboxCommandDispatcher
{
private readonly AppearanceService _appearance;
private readonly WindowsUpdateService _updates;
private readonly WindowsSecurityService _security;
private readonly AutoLogonService _autoLogon;
private readonly DesktopArchiveService _desktop;
private readonly ActivationService _activation;
private readonly OperationLogStore _logs;
public ToolboxCommandDispatcher()
{
var registry = new RegistryStore();
var snapshots = new SnapshotStore();
var processes = new ProcessRunner();
var tasks = new TaskSchedulerService();
_appearance = new AppearanceService(registry, snapshots);
_updates = new WindowsUpdateService(registry, snapshots, processes, tasks);
_security = new WindowsSecurityService(registry, snapshots, processes);
_autoLogon = new AutoLogonService(registry, snapshots);
_desktop = new DesktopArchiveService(tasks);
_activation = new ActivationService(registry, snapshots, processes);
_logs = new OperationLogStore();
}
public static bool RequiresElevation(ToolboxCommandKind kind) => kind is not
(ToolboxCommandKind.ArchiveDesktop or
ToolboxCommandKind.UndoDesktopArchive or
ToolboxCommandKind.ConfigureDesktopSchedule or
ToolboxCommandKind.DetectStatus);
public async Task<CommandResult> ExecuteAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
using var operationLock = new Semaphore(1, 1, @"Local\WindowsToolbox.OperationLock", out _);
var acquired = await Task.Run(() => operationLock.WaitOne(TimeSpan.FromMinutes(2)), cancellationToken);
if (!acquired)
{
return new CommandResult
{
CommandId = command.Id,
Kind = command.Kind,
Success = false,
Summary = "另一个 Windows 工具箱操作仍在执行",
Steps = [new CommandStepResult("并发检查", false, "等待两分钟后仍未取得操作锁")]
};
}
try
{
CommandResult result;
if (RequiresElevation(command.Kind) && !SystemStatusService.IsAdministrator())
{
result = new CommandResult
{
CommandId = command.Id,
Kind = command.Kind,
Success = false,
Summary = "该操作需要管理员权限",
Steps = [new CommandStepResult("权限检查", false, "请通过主程序的管理员授权提示执行")]
};
}
else
{
result = command.Kind switch
{
ToolboxCommandKind.EnableClassicContextMenu => await _appearance.EnableClassicContextMenuAsync(command, cancellationToken),
ToolboxCommandKind.RestoreContextMenu => await _appearance.RestoreContextMenuAsync(command, cancellationToken),
ToolboxCommandKind.HideShortcutArrow => await _appearance.HideShortcutArrowAsync(command, cancellationToken),
ToolboxCommandKind.RestoreShortcutArrow => await _appearance.RestoreShortcutArrowAsync(command, cancellationToken),
ToolboxCommandKind.PauseWindowsUpdate => await _updates.PauseAsync(command, cancellationToken),
ToolboxCommandKind.DisableWindowsUpdate => await _updates.DisableAsync(command, cancellationToken),
ToolboxCommandKind.RestoreWindowsUpdate => await _updates.RestoreAsync(command, cancellationToken),
ToolboxCommandKind.ConfigureUpdateRenewal => await _updates.ConfigureRenewalAsync(command, cancellationToken),
ToolboxCommandKind.DisableDefenderProtection => await _security.DisableAsync(command, cancellationToken),
ToolboxCommandKind.RestoreDefenderProtection => await _security.RestoreAsync(command, cancellationToken),
ToolboxCommandKind.PrepareAutoLogon => await _autoLogon.PrepareAsync(command, cancellationToken),
ToolboxCommandKind.RestoreAutoLogonPreparation => await _autoLogon.RestoreAsync(command, cancellationToken),
ToolboxCommandKind.ArchiveDesktop => await _desktop.ArchiveAsync(command, cancellationToken),
ToolboxCommandKind.UndoDesktopArchive => await _desktop.UndoLatestAsync(command, cancellationToken),
ToolboxCommandKind.ConfigureDesktopSchedule => await _desktop.ConfigureScheduleAsync(command, cancellationToken),
ToolboxCommandKind.ActivateWindowsKms => await _activation.ActivateAsync(command, cancellationToken),
ToolboxCommandKind.RestoreKmsConfiguration => await _activation.RestoreAsync(command, cancellationToken),
_ => new CommandResult
{
CommandId = command.Id,
Kind = command.Kind,
Success = false,
Summary = "不支持的操作",
Steps = [new CommandStepResult("命令分派", false, command.Kind.ToString())]
}
};
}
try
{
await _logs.AddAsync(new OperationLogEntry
{
Id = result.CommandId,
Feature = result.Kind.ToString(),
Action = result.Kind.ToString(),
Success = result.Success,
Summary = result.Summary,
Steps = result.Steps
}, cancellationToken);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
result.Steps.Add(new CommandStepResult("操作历史", false, "功能已执行,但日志写入失败:" + ex.Message));
}
if (!string.IsNullOrWhiteSpace(command.ResultPath))
{
await JsonFileStore.SaveAsync(command.ResultPath, result, cancellationToken);
}
return result;
}
finally
{
operationLock.Release();
}
}
}
@@ -0,0 +1,209 @@
using System.Text.Json;
using Microsoft.Win32;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class WindowsSecurityService(
RegistryStore registry,
SnapshotStore snapshots,
ProcessRunner processes)
{
private const string Feature = "windows-security";
private const string NotificationKey = @"SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Notifications";
public async Task<CommandResult> DisableAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
var steps = new List<CommandStepResult>();
try
{
await snapshots.GetOrCreateAsync(Feature, () => CaptureBaselineAsync(cancellationToken), cancellationToken);
var disableFirewall = command.Arguments.TryGetValue("firewall", out var raw) && bool.TryParse(raw, out var parsed) && parsed;
var tamper = await processes.RunPowerShellAsync(
"$s=Get-MpComputerStatus -ErrorAction Stop; [pscustomobject]@{IsTamperProtected=$s.IsTamperProtected}|ConvertTo-Json -Compress",
cancellationToken);
var tamperEnabled = ParseBoolean(tamper.StandardOutput, "IsTamperProtected");
if (tamperEnabled == true)
{
steps.Add(new CommandStepResult("防篡改保护", false,
"防篡改保护仍然开启。请先在“Windows 安全中心 → 病毒和威胁防护 → 管理设置”中手动关闭。"));
return Build(command, steps, "Windows 阻止了 Defender 设置修改");
}
var defenderScript = "$ErrorActionPreference='Stop'; " +
"Set-MpPreference -DisableRealtimeMonitoring $true -DisableBehaviorMonitoring $true " +
"-DisableBlockAtFirstSeen $true -DisableIOAVProtection $true -DisableScriptScanning $true " +
"-MAPSReporting 0 -SubmitSamplesConsent 2; " +
"$p=Get-MpPreference; [pscustomobject]@{Disabled=$p.DisableRealtimeMonitoring}|ConvertTo-Json -Compress";
var defender = await processes.RunPowerShellAsync(defenderScript, cancellationToken);
var defenderDisabled = defender.Success && ParseBoolean(defender.StandardOutput, "Disabled") == true;
steps.Add(new CommandStepResult("Defender 防护", defenderDisabled,
defenderDisabled ? "实时、行为、脚本、IOAV 和云端样本防护已关闭" : Friendly(defender, "Defender 修改失败")));
registry.Set("HKLM", NotificationKey, "DisableNotifications", 1, RegistryValueKind.DWord);
registry.Set("HKLM", NotificationKey, "DisableEnhancedNotifications", 1, RegistryValueKind.DWord);
steps.Add(new CommandStepResult("安全通知", true, "安全中心通知策略已关闭"));
if (disableFirewall)
{
var firewall = await processes.RunPowerShellAsync(
"$ErrorActionPreference='Stop'; Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False",
cancellationToken);
steps.Add(new CommandStepResult("Windows 防火墙", firewall.Success,
firewall.Success ? "三个网络配置文件的防火墙均已关闭" : Friendly(firewall, "防火墙修改失败")));
}
return Build(command, steps, defenderDisabled ? "Windows 防护组件已按所选范围关闭" : "部分安全设置未能关闭");
}
catch (Exception ex)
{
steps.Add(new CommandStepResult("关闭安全防护", false, ex.Message));
return Build(command, steps, "关闭 Windows 防护失败");
}
}
public async Task<CommandResult> RestoreAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
var steps = new List<CommandStepResult>();
try
{
var snapshot = await snapshots.LoadAsync(Feature, cancellationToken);
if (snapshot is null)
{
registry.DeleteValue("HKLM", NotificationKey, "DisableNotifications");
registry.DeleteValue("HKLM", NotificationKey, "DisableEnhancedNotifications");
steps.Add(new CommandStepResult("安全通知", true, "已清除本工具使用的通知策略"));
return Build(command, steps, "没有找到 Defender 基线快照;只清除了工具策略");
}
if (snapshot.Metadata.TryGetValue("defender", out var defenderJson) && !string.IsNullOrWhiteSpace(defenderJson))
{
var script = BuildDefenderRestoreScript(defenderJson);
var defender = await processes.RunPowerShellAsync(script, cancellationToken);
steps.Add(new CommandStepResult("Defender 防护", defender.Success,
defender.Success ? "Defender 偏好已恢复" : Friendly(defender, "Defender 恢复失败,可能仍受防篡改保护阻止")));
}
if (snapshot.Metadata.TryGetValue("firewall", out var firewallJson) && !string.IsNullOrWhiteSpace(firewallJson))
{
var script = BuildFirewallRestoreScript(firewallJson);
var firewall = await processes.RunPowerShellAsync(script, cancellationToken);
steps.Add(new CommandStepResult("Windows 防火墙", firewall.Success,
firewall.Success ? "防火墙配置文件状态已恢复" : Friendly(firewall, "防火墙恢复失败")));
}
foreach (var value in snapshot.RegistryValues)
{
registry.Restore(value);
}
steps.Add(new CommandStepResult("安全通知", true, "安全中心通知策略已恢复"));
if (steps.All(x => x.Success))
{
snapshots.Delete(Feature);
}
return Build(command, steps, steps.All(x => x.Success) ? "Windows 安全设置已恢复" : "部分安全设置恢复失败");
}
catch (Exception ex)
{
steps.Add(new CommandStepResult("恢复安全设置", false, ex.Message));
return Build(command, steps, "恢复 Windows 安全设置失败");
}
}
private async Task<FeatureSnapshot> CaptureBaselineAsync(CancellationToken cancellationToken)
{
var defender = await processes.RunPowerShellAsync(
"$p=Get-MpPreference -ErrorAction Stop; [pscustomobject]@{" +
"DisableRealtimeMonitoring=$p.DisableRealtimeMonitoring;" +
"DisableBehaviorMonitoring=$p.DisableBehaviorMonitoring;" +
"DisableBlockAtFirstSeen=$p.DisableBlockAtFirstSeen;" +
"DisableIOAVProtection=$p.DisableIOAVProtection;" +
"DisableScriptScanning=$p.DisableScriptScanning;" +
"MAPSReporting=[int]$p.MAPSReporting;" +
"SubmitSamplesConsent=[int]$p.SubmitSamplesConsent}|ConvertTo-Json -Compress", cancellationToken);
var firewall = await processes.RunPowerShellAsync(
"Get-NetFirewallProfile | Select-Object Name,Enabled | ConvertTo-Json -Compress", cancellationToken);
return new FeatureSnapshot
{
Id = Guid.NewGuid().ToString("N"),
Feature = Feature,
RegistryValues =
[
registry.Capture("HKLM", NotificationKey, "DisableNotifications"),
registry.Capture("HKLM", NotificationKey, "DisableEnhancedNotifications")
],
Metadata =
{
["defender"] = defender.Success ? defender.StandardOutput.Trim() : null,
["firewall"] = firewall.Success ? firewall.StandardOutput.Trim() : null
}
};
}
private static string BuildDefenderRestoreScript(string json)
{
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
static string PsBool(bool value) => value ? "$true" : "$false";
bool Bool(JsonElement element, string name) => element.TryGetProperty(name, out var value) && value.GetBoolean();
int Int(JsonElement element, string name) => element.TryGetProperty(name, out var value) ? value.GetInt32() : 0;
return "$ErrorActionPreference='Stop'; Set-MpPreference " +
$"-DisableRealtimeMonitoring {PsBool(Bool(root, "DisableRealtimeMonitoring"))} " +
$"-DisableBehaviorMonitoring {PsBool(Bool(root, "DisableBehaviorMonitoring"))} " +
$"-DisableBlockAtFirstSeen {PsBool(Bool(root, "DisableBlockAtFirstSeen"))} " +
$"-DisableIOAVProtection {PsBool(Bool(root, "DisableIOAVProtection"))} " +
$"-DisableScriptScanning {PsBool(Bool(root, "DisableScriptScanning"))} " +
$"-MAPSReporting {Int(root, "MAPSReporting")} " +
$"-SubmitSamplesConsent {Int(root, "SubmitSamplesConsent")}";
}
private static string BuildFirewallRestoreScript(string json)
{
using var document = JsonDocument.Parse(json);
var items = document.RootElement.ValueKind == JsonValueKind.Array
? document.RootElement.EnumerateArray().ToArray()
: [document.RootElement];
var commands = new List<string> { "$ErrorActionPreference='Stop'" };
foreach (var item in items)
{
var name = item.GetProperty("Name").GetString();
if (name is not ("Domain" or "Public" or "Private"))
{
continue;
}
var enabled = item.GetProperty("Enabled").GetBoolean() ? "$true" : "$false";
commands.Add($"Set-NetFirewallProfile -Profile {name} -Enabled {enabled}");
}
return string.Join("; ", commands);
}
private static bool? ParseBoolean(string json, string property)
{
try
{
using var document = JsonDocument.Parse(json);
return document.RootElement.TryGetProperty(property, out var value) ? value.GetBoolean() : null;
}
catch
{
return null;
}
}
private static CommandResult Build(ToolboxCommand command, List<CommandStepResult> steps, string summary) => new()
{
CommandId = command.Id,
Kind = command.Kind,
Success = steps.Count > 0 && steps.All(x => x.Success),
Summary = summary,
Steps = steps
};
private static string Friendly(ProcessResult result, string fallback) =>
string.IsNullOrWhiteSpace(result.CombinedOutput) ? fallback : result.CombinedOutput.Trim();
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\WindowsToolbox.Core\WindowsToolbox.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,283 @@
using Microsoft.Win32;
using WindowsToolbox.Core;
namespace WindowsToolbox.Infrastructure;
public sealed class WindowsUpdateService(
RegistryStore registry,
SnapshotStore snapshots,
ProcessRunner processes,
TaskSchedulerService tasks)
{
private const string Feature = "windows-update";
private const string AuPolicyKey = @"SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU";
private const string UxSettingsKey = @"SOFTWARE\Microsoft\WindowsUpdate\UX\Settings";
private const string UpdateGuardTask = "UpdateGuard";
private const string PauseRenewTask = "UpdatePauseRenew";
private static readonly string[] PauseValueNames =
[
"PauseUpdatesStartTime", "PauseUpdatesExpiryTime",
"PauseFeatureUpdatesStartTime", "PauseFeatureUpdatesEndTime",
"PauseQualityUpdatesStartTime", "PauseQualityUpdatesEndTime"
];
private static readonly string[] Services = ["wuauserv", "UsoSvc", "WaaSMedicSvc"];
private static readonly string[] UpdateTasks =
[
@"\Microsoft\Windows\UpdateOrchestrator\Schedule Scan",
@"\Microsoft\Windows\UpdateOrchestrator\Schedule Maintenance Work",
@"\Microsoft\Windows\UpdateOrchestrator\USO_UxBroker",
@"\Microsoft\Windows\WindowsUpdate\Scheduled Start",
@"\Microsoft\Windows\WindowsUpdate\sih",
@"\Microsoft\Windows\WindowsUpdate\sihboot"
];
public async Task<CommandResult> PauseAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
var steps = new List<CommandStepResult>();
try
{
var days = int.TryParse(command.Arguments.GetValueOrDefault("days"), out var parsed) ? parsed : 35;
days = Math.Clamp(days, 1, 35);
var snapshot = await GetBaselineAsync(cancellationToken);
await RestoreControlledStateAsync(snapshot, restoreRunningServices: true, cancellationToken);
var start = DateTimeOffset.UtcNow;
var end = start.AddDays(days);
registry.Set("HKLM", AuPolicyKey, "NoAutoUpdate", 0, RegistryValueKind.DWord);
registry.Set("HKLM", AuPolicyKey, "AUOptions", 2, RegistryValueKind.DWord);
SetPauseValues(start, end);
tasks.DeleteToolboxTask(UpdateGuardTask);
var actual = registry.Read("HKLM", UxSettingsKey, "PauseUpdatesExpiryTime")?.ToString();
var verified = !string.IsNullOrWhiteSpace(actual);
steps.Add(new CommandStepResult("暂停更新", verified,
verified ? $"已请求暂停至 {end.ToLocalTime():yyyy-MM-dd HH:mm}" : "暂停时间写入后验证失败"));
return Build(command, steps, verified ? "Windows 更新已暂停" : "Windows 未接受暂停设置");
}
catch (Exception ex)
{
steps.Add(new CommandStepResult("暂停更新", false, ex.Message));
return Build(command, steps, "暂停更新失败");
}
}
public async Task<CommandResult> DisableAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
var steps = new List<CommandStepResult>();
try
{
await GetBaselineAsync(cancellationToken);
registry.Set("HKLM", AuPolicyKey, "NoAutoUpdate", 1, RegistryValueKind.DWord);
registry.Set("HKLM", AuPolicyKey, "AUOptions", 2, RegistryValueKind.DWord);
ClearPauseValues();
steps.Add(new CommandStepResult("更新策略", true, "已设置禁止自动更新策略"));
foreach (var service in Services)
{
var stop = await processes.RunAsync("sc.exe", ["stop", service], cancellationToken);
var disable = await processes.RunAsync("sc.exe", ["config", service, "start=", "disabled"], cancellationToken);
var success = disable.Success;
var message = success
? "已停止并禁用"
: FriendlyOutput(disable, "系统拒绝修改;通常是受保护服务");
steps.Add(new CommandStepResult($"服务 {service}", success, message));
_ = stop;
}
foreach (var taskPath in UpdateTasks)
{
if (tasks.TrySetEnabled(taskPath, false, out var message))
{
steps.Add(new CommandStepResult($"任务 {ShortTaskName(taskPath)}", true, message));
}
else
{
steps.Add(new CommandStepResult($"任务 {ShortTaskName(taskPath)}", false, message));
}
}
tasks.DeleteToolboxTask(PauseRenewTask);
if (!command.Arguments.ContainsKey("guard"))
{
var host = CompanionLocator.Find("WindowsToolbox.ElevatedHost.exe");
tasks.RegisterDailyTask(UpdateGuardTask, host, "--scheduled-update-disable", new TimeOnly(9, 15), highestPrivileges: true);
steps.Add(new CommandStepResult("关闭守护任务", true, "每天检查一次更新关闭状态"));
}
var policyDisabled = Convert.ToInt32(registry.Read("HKLM", AuPolicyKey, "NoAutoUpdate") ?? 0) == 1;
var updateServiceDisabled = Convert.ToInt32(registry.Read(
"HKLM", @"SYSTEM\CurrentControlSet\Services\wuauserv", "Start") ?? 0) == 4;
var disabled = policyDisabled && updateServiceDisabled;
steps.Add(new CommandStepResult("结果验证", disabled,
disabled ? "关闭策略和 Windows Update 服务均已验证" : "策略或 Windows Update 服务没有进入关闭状态"));
return Build(command, steps, disabled ? "Windows 自动更新已强力关闭" : "Windows 自动更新未完全关闭,请查看失败步骤");
}
catch (Exception ex)
{
steps.Add(new CommandStepResult("关闭更新", false, ex.Message));
return Build(command, steps, "关闭 Windows 更新失败");
}
}
public async Task<CommandResult> RestoreAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
var steps = new List<CommandStepResult>();
try
{
var snapshot = await snapshots.LoadAsync(Feature, cancellationToken);
if (snapshot is null)
{
registry.DeleteValue("HKLM", AuPolicyKey, "NoAutoUpdate");
registry.DeleteValue("HKLM", AuPolicyKey, "AUOptions");
ClearPauseValues();
steps.Add(new CommandStepResult("恢复策略", true, "没有基线快照,已清除本工具使用的策略"));
}
else
{
await RestoreControlledStateAsync(snapshot, restoreRunningServices: true, cancellationToken);
steps.Add(new CommandStepResult("恢复策略与服务", true, "已恢复执行工具前的状态"));
}
tasks.DeleteToolboxTask(UpdateGuardTask);
tasks.DeleteToolboxTask(PauseRenewTask);
snapshots.Delete(Feature);
return Build(command, steps, "Windows 更新设置已恢复");
}
catch (Exception ex)
{
steps.Add(new CommandStepResult("恢复更新", false, ex.Message));
return Build(command, steps, "恢复 Windows 更新失败");
}
}
public Task<CommandResult> ConfigureRenewalAsync(ToolboxCommand command, CancellationToken cancellationToken = default)
{
var steps = new List<CommandStepResult>();
try
{
var enabled = command.Arguments.TryGetValue("enabled", out var value) && bool.Parse(value);
var days = int.TryParse(command.Arguments.GetValueOrDefault("days"), out var parsed) ? Math.Clamp(parsed, 1, 35) : 35;
if (enabled)
{
var host = CompanionLocator.Find("WindowsToolbox.ElevatedHost.exe");
tasks.RegisterDailyTask(PauseRenewTask, host, $"--scheduled-update-renew {days}", new TimeOnly(9, 0), highestPrivileges: true);
steps.Add(new CommandStepResult("自动续期", true, $"每天检查并重新请求暂停 {days} 天"));
}
else
{
tasks.DeleteToolboxTask(PauseRenewTask);
steps.Add(new CommandStepResult("自动续期", true, "已关闭暂停自动续期"));
}
return Task.FromResult(Build(command, steps, enabled ? "更新暂停自动续期已启用" : "更新暂停自动续期已关闭"));
}
catch (Exception ex)
{
steps.Add(new CommandStepResult("自动续期", false, ex.Message));
return Task.FromResult(Build(command, steps, "自动续期配置失败"));
}
}
private async Task<FeatureSnapshot> GetBaselineAsync(CancellationToken cancellationToken)
{
return await snapshots.GetOrCreateAsync(Feature, async () =>
{
var snapshot = new FeatureSnapshot
{
Id = Guid.NewGuid().ToString("N"),
Feature = Feature
};
snapshot.RegistryValues.Add(registry.Capture("HKLM", AuPolicyKey, "NoAutoUpdate"));
snapshot.RegistryValues.Add(registry.Capture("HKLM", AuPolicyKey, "AUOptions"));
foreach (var name in PauseValueNames)
{
snapshot.RegistryValues.Add(registry.Capture("HKLM", UxSettingsKey, name));
}
foreach (var service in Services)
{
snapshot.RegistryValues.Add(registry.Capture("HKLM", $@"SYSTEM\CurrentControlSet\Services\{service}", "Start"));
var result = await processes.RunAsync("sc.exe", ["query", service], cancellationToken);
snapshot.Metadata[$"service-running:{service}"] =
System.Text.RegularExpressions.Regex.IsMatch(
result.StandardOutput,
@"STATE\s*:\s*4\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase).ToString();
}
foreach (var taskPath in UpdateTasks)
{
var enabled = tasks.TryGetEnabled(taskPath);
snapshot.Metadata[$"task:{taskPath}"] = enabled?.ToString();
}
return snapshot;
}, cancellationToken);
}
private async Task RestoreControlledStateAsync(FeatureSnapshot snapshot, bool restoreRunningServices, CancellationToken cancellationToken)
{
foreach (var value in snapshot.RegistryValues)
{
registry.Restore(value);
}
foreach (var taskPath in UpdateTasks)
{
if (snapshot.Metadata.TryGetValue($"task:{taskPath}", out var raw) && bool.TryParse(raw, out var enabled))
{
tasks.TrySetEnabled(taskPath, enabled, out _);
}
}
if (!restoreRunningServices)
{
return;
}
foreach (var service in Services)
{
if (snapshot.Metadata.TryGetValue($"service-running:{service}", out var raw) && bool.TryParse(raw, out var wasRunning) && wasRunning)
{
await processes.RunAsync("sc.exe", ["start", service], cancellationToken);
}
}
}
private void SetPauseValues(DateTimeOffset start, DateTimeOffset end)
{
var startText = start.ToString("yyyy-MM-ddTHH:mm:ssZ");
var endText = end.ToString("yyyy-MM-ddTHH:mm:ssZ");
registry.Set("HKLM", UxSettingsKey, "PauseUpdatesStartTime", startText, RegistryValueKind.String);
registry.Set("HKLM", UxSettingsKey, "PauseUpdatesExpiryTime", endText, RegistryValueKind.String);
registry.Set("HKLM", UxSettingsKey, "PauseFeatureUpdatesStartTime", startText, RegistryValueKind.String);
registry.Set("HKLM", UxSettingsKey, "PauseFeatureUpdatesEndTime", endText, RegistryValueKind.String);
registry.Set("HKLM", UxSettingsKey, "PauseQualityUpdatesStartTime", startText, RegistryValueKind.String);
registry.Set("HKLM", UxSettingsKey, "PauseQualityUpdatesEndTime", endText, RegistryValueKind.String);
}
private void ClearPauseValues()
{
foreach (var name in PauseValueNames)
{
registry.DeleteValue("HKLM", UxSettingsKey, name);
}
}
private static CommandResult Build(ToolboxCommand command, List<CommandStepResult> steps, string summary) => new()
{
CommandId = command.Id,
Kind = command.Kind,
Success = steps.Count > 0 && steps.Any(x => x.Success) && steps.Last().Success,
Summary = summary,
Steps = steps
};
private static string ShortTaskName(string path) => path[(path.LastIndexOf('\\') + 1)..];
private static string FriendlyOutput(ProcessResult result, string fallback) =>
string.IsNullOrWhiteSpace(result.CombinedOutput) ? fallback : result.CombinedOutput.Trim();
}
@@ -0,0 +1,124 @@
using System.Text.Json;
using WindowsToolbox.Core;
using WindowsToolbox.Infrastructure;
var failures = new List<string>();
Check(ToolboxCommandDispatcher.RequiresElevation(ToolboxCommandKind.DisableWindowsUpdate),
"关闭更新必须提权");
Check(!ToolboxCommandDispatcher.RequiresElevation(ToolboxCommandKind.ArchiveDesktop),
"桌面归档不应要求提权");
var command = new ToolboxCommand
{
Id = "verification",
Kind = ToolboxCommandKind.PauseWindowsUpdate,
Arguments = new Dictionary<string, string> { ["days"] = "35" }
};
var roundTrip = JsonSerializer.Deserialize<ToolboxCommand>(
JsonSerializer.Serialize(command, JsonDefaults.Options), JsonDefaults.Options);
Check(roundTrip?.Kind == ToolboxCommandKind.PauseWindowsUpdate, "命令 JSON 往返失败");
Check(roundTrip?.Arguments.GetValueOrDefault("days") == "35", "命令参数 JSON 往返失败");
var activation = new ActivationService(new RegistryStore(), new SnapshotStore(), new ProcessRunner());
var catalog = activation.LoadCatalog();
Check(catalog.Any(x => x.EditionId == "Professional" && x.ProductKey == "W269N-WFGWX-YVC9B-4J6C9-T83GX"),
"KMS 专业版目录缺失");
Check(catalog.Any(x => x.EditionId == "Core" && !x.Supported),
"家庭版应明确标记为不支持 KMS");
Check(catalog.Where(x => x.Supported).All(x => x.ProductKey.Length == 29),
"KMS 客户端密钥格式错误");
var temporaryRoot = Path.Combine(Path.GetTempPath(), "WindowsToolbox-Verification-" + Guid.NewGuid().ToString("N"));
try
{
var testDesktop = Path.Combine(temporaryRoot, "Desktop");
var testDestination = Path.Combine(temporaryRoot, "Archive");
Directory.CreateDirectory(testDesktop);
File.WriteAllText(Path.Combine(testDesktop, "report.txt"), "first");
File.WriteAllText(Path.Combine(testDesktop, "keep.lnk"), "shortcut");
var projectFolder = Path.Combine(testDesktop, "Project");
Directory.CreateDirectory(projectFolder);
File.WriteAllText(Path.Combine(projectFolder, "first.txt"), "folder-first");
var systemFolder = Path.Combine(testDesktop, "SystemFolder");
Directory.CreateDirectory(systemFolder);
File.SetAttributes(systemFolder, File.GetAttributes(systemFolder) | FileAttributes.System);
var desktopService = new DesktopArchiveService(
new TaskSchedulerService(),
() => testDesktop,
Path.Combine(temporaryRoot, "Data"));
var archiveCommand = new ToolboxCommand
{
Id = Guid.NewGuid().ToString("N"),
Kind = ToolboxCommandKind.ArchiveDesktop,
Arguments = new Dictionary<string, string>
{
["profile"] = "verification",
["destination"] = testDestination
}
};
var firstArchive = await desktopService.ArchiveAsync(archiveCommand);
var dateFolder = Path.Combine(testDestination, DateTime.Now.ToString("yyyy-MM-dd"));
Check(firstArchive.Success, "第一次桌面归档失败");
Check(File.Exists(Path.Combine(dateFolder, "report.txt")), "普通文件没有归档");
Check(File.Exists(Path.Combine(testDesktop, "keep.lnk")), "快捷方式不应被移动");
Check(Directory.Exists(Path.Combine(dateFolder, "Project")), "普通文件夹没有归档");
Check(Directory.Exists(systemFolder), "系统文件夹不应被移动");
await Task.Delay(5);
File.WriteAllText(Path.Combine(testDesktop, "report.txt"), "second");
Directory.CreateDirectory(projectFolder);
File.WriteAllText(Path.Combine(projectFolder, "second.txt"), "folder-second");
var secondArchive = await desktopService.ArchiveAsync(archiveCommand with { Id = Guid.NewGuid().ToString("N") });
Check(secondArchive.Success, "第二次桌面归档失败");
Check(File.Exists(Path.Combine(dateFolder, "report (1).txt")), "同一天同名文件没有自动编号");
Check(Directory.Exists(Path.Combine(dateFolder, "Project (1)")), "同一天同名文件夹没有自动编号");
await Task.Delay(5);
Directory.CreateDirectory(projectFolder);
File.WriteAllText(Path.Combine(projectFolder, "third.txt"), "folder-third");
var thirdArchive = await desktopService.ArchiveAsync(archiveCommand with { Id = Guid.NewGuid().ToString("N") });
Check(thirdArchive.Success, "第三次桌面归档失败");
Check(Directory.Exists(Path.Combine(dateFolder, "Project (2)")), "多次同名文件夹没有继续自动编号");
var undo = await desktopService.UndoLatestAsync(new ToolboxCommand
{
Id = Guid.NewGuid().ToString("N"),
Kind = ToolboxCommandKind.UndoDesktopArchive
});
Check(undo.Success, "撤销最近桌面归档失败");
Check(File.Exists(Path.Combine(testDesktop, "Project", "third.txt")), "撤销没有恢复最近一次文件夹");
Check(File.Exists(Path.Combine(dateFolder, "report.txt")), "撤销最近一次不应影响更早归档");
Check(File.Exists(Path.Combine(dateFolder, "report (1).txt")), "撤销文件夹归档不应影响更早归档文件");
Check(Directory.Exists(Path.Combine(dateFolder, "Project")), "撤销最近一次不应影响更早归档文件夹");
Check(Directory.Exists(Path.Combine(dateFolder, "Project (1)")), "撤销最近一次不应影响第二次归档文件夹");
}
finally
{
if (Directory.Exists(temporaryRoot))
{
foreach (var path in Directory.EnumerateFileSystemEntries(temporaryRoot, "*", SearchOption.AllDirectories))
{
try { File.SetAttributes(path, FileAttributes.Normal); } catch { }
}
Directory.Delete(temporaryRoot, recursive: true);
}
}
if (failures.Count > 0)
{
Console.Error.WriteLine(string.Join(Environment.NewLine, failures));
return 1;
}
Console.WriteLine($"验证通过:{catalog.Count} 个 KMS 版本条目;命令模型、权限边界、文件与文件夹归档、系统项排除、同名处理和撤销正常。");
return 0;
void Check(bool condition, string message)
{
if (!condition)
{
failures.Add("失败: " + message);
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\src\WindowsToolbox.Core\WindowsToolbox.Core.csproj" />
<ProjectReference Include="..\..\src\WindowsToolbox.Infrastructure\WindowsToolbox.Infrastructure.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>