今天接着用 Claude 把百度网盘和夸克网盘也做成了绿色版,分别放在 D:GreenBaiduNetdisk 和 D:GreenQuark。和 Claude Desktop、DeepSeek Harness 不同,这两个我不要自动下载更新,有新版本时我自己把安装包放进 DataPackages。
一开始就有意外:我放进去的夸克安装包只有 4MB,是在线安装器,里面根本没有程序。Claude 从它里面找到一个备用下载地址,下到了 7.4.2.822 的完整安装包。两个安装包的格式也比前两个难拆:百度网盘是 NSIS,夸克是 Inno Setup,系统自带的工具都解不开,于是在 D:Green7-Zip 和 D:Greeninnoextract 放了两个绿色版解包工具。我问 WinRAR 能不能代替,实测不行。
更麻烦的是数据位置。这两个软件都没有指定数据目录的开关,Claude 只能把程序实际运行一次,对比运行前后的文件和注册表,才找出它们把数据写在哪里,最后用目录联接把这些目录链接到 Data 里。百度网盘还会往自己的程序目录里写设置和账号数据,更新时要保留;夸克每次启动都会往注册表里写一大堆东西,每次测试完都要清理。
我问为什么这两个花的时间是前两个的几倍,Claude 说大部分是软件本身难,也有它自己的几次失误,比如快照脚本写错卡了二十多分钟、保留文件的功能一开始一个都没保留。
做完我实际试了:两个软件都能用,登录以后重启电脑,登录信息和配置都还在。
最后做了整理:在 D:GreenGreenCreator 放了找数据位置、测试、查残留的三个工具和一份说明;在两个软件目录里各放了一份 DownloadURL.txt,写着完整安装包的下载地址;把要点存进了记忆库;还把这次的经验写成了一篇文档《桌面软件绿色化实战手册》:
桌面软件绿色化实战手册
2026-11-08
Claude Desktop、DeepSeek Harness、百度网盘、夸克网盘四款软件都已做成绿色版:程序在 App、数据在 Data,C 盘重启还原后登录和配置都还在。一个软件好不好做,取决于三件事:安装包能不能不运行就拆开、数据位置有没有现成的开关、软件会不会把自己写进系统。
四个软件一览
四个软件用的是同一套结构:一个 <名称>PortableSetup.bat 生成启动器、更新入口和更新脚本。不同的只是下表这几项。
| 软件 | 技术 | 安装包 | 解包 | 数据放进 Data 的方式 | 关闭自带更新 | 更新方式 |
|---|---|---|---|---|---|---|
| Claude Desktop | Electron | MSIX | .NET 的 zip 读取 | --user-data-dir + CLAUDE_CONFIG_DIR | 便携运行时未发现自带更新 | 自动下载 |
| DeepSeek Harness | Electron | NSIS,内嵌 7z | 切出 7z,用系统自带的 tar.exe | --user-data-dir + DSH_HOME、DSH_AGENTS_HOME | 删除 app-update.yml | 自动下载 |
| 百度网盘 | 传统客户端 + 内嵌 Chromium | NSIS,LZMA 整体压缩 | 7-Zip | 目录联接(AppData 两个目录、App\users) | IsAutoUpdate=0 | 手动放安装包 |
| 夸克网盘 | 定制的 Chromium | Inno Setup 6.1 | innoextract | 目录联接(LocalAppData 三个目录) | 删除更新器的安装程序 | 手动放安装包 |
自动下载的两个软件有官方的版本来源,脚本不用下载就能知道有没有新版本。手动的两个由你把完整安装包放进 Data\Packages,脚本选用其中版本最高、签名正确的一个。
难度从哪来
百度网盘和夸克花的时间是前两个的几倍,大部分来自软件本身,小部分来自制作过程中的失误。
| 方面 | Claude / DeepSeek | 百度网盘 / 夸克 |
|---|---|---|
| 安装包 | MSIX 就是 zip;DeepSeek 内嵌的 7z 用系统自带的 tar 就能解 | NSIS 的 LZMA 整体压缩、Inno Setup 都要外部工具;夸克还先拿到一个没有程序的在线安装器 |
| 数据位置 | 在程序里一搜就有专用的环境变量 | 没有可用的开关,只能实际运行一次、对比前后快照找出来;夸克的 --user-data-dir 只管一部分数据 |
| 程序目录 | 只读 | 百度网盘会在 App 里写设置文件和账号数据,更新时要保留 |
| 写入系统 | 很少 | 夸克每次启动都写大量注册表项,每轮测试后都要清理 |
| 文件名 | 安装包里就是真名 | 夸克有 3 个文件名安装时才算出来,要从程序代码里查 |
失误造成的返工:
- 快照脚本通配符写错(
-like '[*')导致卡住,又递归扫描整个用户目录,一次用了 20 多分钟。 - 测试目录路径太长,超过 260 字符,夸克第一次解包失败。
- “保留运行中产生的文件”最初一个都没保留(PowerShell 函数返回列表的坑),重新部署会清掉百度网盘的设置。
- 链接检查最初只看标记是否存在,链接指向旧位置时会误判为有效。
- 测试后忘了关掉夸克,文件被占用,清理只好重做一次。
这些经验已经写进 GreenCreator 的工具和说明里。
做一个新软件的步骤
从拿到安装包到交付,按下面八步做;前三步只是了解软件,不用写代码。
- 拿到完整安装包,核对签名。 用
Get-AuthenticodeSignature看签名是否有效、签名者是谁。只有几 MB 的通常是在线安装器。 - 认出安装包格式,把程序解到测试目录。 不运行安装程序:它会写注册表、把程序装进系统目录。
- 查清数据位置。 先在程序文件里搜,再用
Probe-App.ps1实际运行一次核对,还要看程序会不会往自己的目录里写文件。 - 选定把数据放进 Data 的办法(见下一节)。
- 找到关闭自带更新的办法。 优先用软件自己的配置项或控制更新的文件。
- 写 Setup。 从最接近的现有 Setup 复制一份改:自动下载的参照 Claude 或 DeepSeek Harness,手动放安装包的参照百度网盘或夸克。
- 在测试目录里测试(见“测试与清理”)。
- 清理测试残留,再在真实目录运行 Setup。
各种安装包的解法:
| 格式 | 怎么认 | 解法 |
|---|---|---|
| MSIX | 扩展名 .msix,本质是 zip | 读取 app/ 下的条目,文件名要做 URL 解码(%40 → @) |
| electron-builder 的 NSIS | 有 NullsoftInst,还有 7z 文件头 | 按文件头算出长度切出 7z,用 System32\tar.exe 解 |
| NSIS(LZMA 整体压缩) | 有 NullsoftInst,数据以 5D 00 00 开头 | 7z x -tNsis,必须加 -tNsis |
| Inno Setup | 有 Inno Setup Setup Data (版本) | innoextract;code$函数名 要查出真名改回来 |
| 在线安装器 | 只有几 MB | 不能用,但里面可能有备用的完整包地址 |
数据放进 Data 的办法
按下面的顺序选,前一种用不了再用后一种;无论哪种,都只管软件自己的数据。
- 软件自己的参数,如 Electron 和 Chromium 的
--user-data-dir。最干净,但夸克说明它不一定管全部数据,要实测。 - 软件自己的环境变量,如
CLAUDE_CONFIG_DIR、DSH_HOME。启动器用setlocal设置,不会泄漏到别处。 - 目录联接:启动器每次启动时,把软件在 C 盘的数据目录链接到
Data\AppData\Roaming\...或Data\AppData\Local\...。软件照常写默认位置,数据实际落在 D 盘;开机自启、文件关联等绕过启动器的启动也不影响。
不要改 USERPROFILE、HOME、APPDATA、LOCALAPPDATA、TEMP。这些变量会影响软件调用的所有程序:git 和 ssh 找不到本机配置,打开浏览器时还会多出一份空白配置。
软件写进自己程序目录的数据(百度网盘就是这样)另有两种处理:
- 整个子目录,如
App\users:部署时联接到Data\users。 - 零散文件,如
AppConfig.ini:每次部署记下安装包里的文件清单(App\.portable-manifest),下次部署时不在清单上的文件就是软件自己生成的,原样搬到新的App。这样不必猜哪些文件是用户数据。
测试与清理
测试一律在 D:\Green\_sb\<名称> 这样的短路径测试目录里做,不碰真实安装;测完必须清理,否则开机时会去启动测试目录里的程序。
| 测试 | 做法 | 能发现的问题 |
|---|---|---|
| 全新安装 / 选 [1] / 选 [2] | Test-Setup.ps1 在真实窗口里运行 | 流程、生成文件、快捷方式;只在真实窗口里出现的 cmd 问题 |
| 通过启动器实际启动 | 启动后检查 Data 和 C 盘的相关目录 | 数据是否全部进了 Data、链接是否正确 |
| 运行中重新部署 | 先启动软件,再运行更新脚本 -Reinstall | 进程能否全部结束、运行中产生的文件是否保留 |
| 已是最新 | 不带参数运行更新脚本 | 是否误判为需要更新 |
| 安装包损坏或不对 | 改动缓存包几个字节;放进在线安装器 | 校验能否拦下,现有程序是否不受影响 |
| 链接位置已有普通文件夹 | 手工建一个同名文件夹再启动 | 启动器是否提示并停下 |
清理时要注意:
- 先关掉测试目录里的程序,否则文件被占用删不掉。
- 用
Find-Leftovers.ps1列出仍指向测试目录的进程、目录链接、开机启动项和注册表项。夸克一次测试会留下约 100 条,包括 HKLM 里的 Chrome 组件。 - 只删测试产生的项:对照
Probe-App.ps1报告里“新的注册表键”和 C 盘文件夹的创建时间,不确定的不删。 - 测试目录里的目录链接先单独删掉,再删整个测试目录。
踩过的坑
下面每一条都在这四个软件里实际碰到过。
| 坑 | 现象 | 做法 |
|---|---|---|
| bat 里的中文 | chcp 65001 之后出现含中文的行,报 “The system cannot find the path specified.”,只在真实窗口里出现 | bat 只用 ASCII,中文提示交给 PowerShell |
| 文件编码 | 无 BOM 的 ps1 里中文乱码;带 BOM 的 bat 第一行识别不了 | bat 无 BOM,ps1 带 BOM,都用 CRLF 换行 |
| MSIX 文件名 | 解出来是 %40ant,原生模块加载不了 | URL 解码文件名 |
| 7-Zip 不认 NSIS | 默认只把安装包当普通程序打开 | 7z x -tNsis |
| 路径太长 | innoextract 写不出文件 | 解包和测试目录都用短路径 |
| 下载中断 | 大文件下到一半连接断开 | 断点续传;用带版本号的地址,避免续传时拼接两个版本 |
| 函数返回列表 | , $list 返回时 foreach 只循环一次,所有项被悄悄跳过 | 直接返回,调用处写 @(函数) |
| 方括号通配符 | -like '[*' 对每一行报错,脚本卡住 | 用 .StartsWith('[') |
| Get-Content 的编码 | 读 UTF-8 中文文件名对不上 | 用 [IO.File]::ReadAllLines 或加 -Encoding UTF8 |
| 链接指向旧位置 | 只检查标记是否存在,误判为有效 | 标记里写目标路径,启动时读回核对 |
| 进程结束不干净 | 结束后又出现子进程,更新报“仍未退出” | 按路径反复结束,最多 15 秒 |
| 按进程名结束 | 误杀别处同名程序(终端里的 Claude Code 也叫 claude.exe) | 按程序路径结束 |
成果和工具在哪里
每个软件的全部内容都在它自己的目录里,通用工具在 D:\Green\GreenCreator。
| 位置 | 内容 |
|---|---|
D:\Green\Claude | ClaudePortableSetup.bat |
D:\Green\DeepSeek Harness | DeepSeekHarnessPortableSetup.bat |
D:\Green\BaiduNetdisk | BaiduNetdiskPortableSetup.bat,DownloadURL.txt(完整安装包下载地址) |
D:\Green\Quark | QuarkPortableSetup.bat,DownloadURL.txt |
D:\Green\GreenCreator | Probe-App.ps1(找数据位置)、Test-Setup.ps1(真实窗口测试)、Find-Leftovers.ps1(查残留)、README.md |
D:\Green\7-Zip | 7-Zip 26.06,解 NSIS |
D:\Green\innoextract | innoextract 1.9,解 Inno Setup |
设计原则的详细说明见 《桌面应用绿色便携化的共性设计》;要点已存入共享记忆库(key 以 green. 和 lesson. 开头)。
附:两个安装脚本
BaiduNetdiskPortableSetup.bat
@echo off
:: Runs the PowerShell section below. Every batch line must stay ASCII-only (cmd misparses Chinese text).
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$lines = [System.IO.File]::ReadAllLines('%~f0', [System.Text.Encoding]::UTF8);" ^
"$start = [Array]::IndexOf($lines, ':::POWERSHELL_BEGIN:::') + 1;" ^
"Invoke-Expression ($lines[$start..($lines.Length - 1)] -join [Environment]::NewLine)"
pause
exit /b
:::POWERSHELL_BEGIN:::
$ErrorActionPreference = "Stop"
$Target = "D:\Green\BaiduNetdisk"
$TargetApp = Join-Path $Target "App"
$UpdatePs1 = Join-Path $Target "BaiduNetdiskPortableUpdate.ps1"
# 生成文件的编码:
# .bat 无 BOM(否则 cmd 无法识别首行),且只含 ASCII 字符,中文提示一律由 PowerShell 输出
# .ps1 带 BOM(否则 Windows PowerShell 5.1 会按 ANSI 编码读取中文)
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$utf8WithBom = New-Object System.Text.UTF8Encoding($true)
# 以 Windows 换行符写出生成的文件
function Save-Text($Name, $Text, $Encoding) {
[System.IO.File]::WriteAllText((Join-Path $Target $Name), (($Text -replace "`r?`n", "`r`n") + "`r`n"), $Encoding)
}
Write-Host "======================================================" -ForegroundColor Cyan
Write-Host " 百度网盘 绿色便携化自动部署向导 " -ForegroundColor Cyan
Write-Host "======================================================" -ForegroundColor Cyan
# 部署方式:默认重新部署;已安装时可改选彻底重置或退出(部署由 BaiduNetdiskPortableUpdate.ps1 完成)
$deployArgs = @{ Reinstall = $true }
if (Test-Path "$TargetApp\BaiduNetdisk.exe") {
Write-Host "`n[提示] 检测到已存在安装版本。" -ForegroundColor Yellow
Write-Host " [1] 重新部署应用核心(保留登录状态与个人配置)"
Write-Host " [2] 彻底全新重置(清空所有数据,仅保留安装包)"
Write-Host " [3] 退出"
$choice = ""
while ($choice -notmatch '^[123]$') { $choice = (Read-Host "`n请选择操作 [1, 2, 3]").Trim() }
if ($choice -eq "3") { Write-Host "已取消操作。" -ForegroundColor Gray; return }
if ($choice -eq "2") { $deployArgs.ResetData = $true }
}
# 1. 写入启动器与更新组件
Write-Host "`n[1/3] 写入启动器与更新组件..." -ForegroundColor Cyan
# 1.1 启动器:百度网盘没有指定数据位置的参数,改为把它在 AppData 下的两个数据目录链接到 Data 里
# %APPDATA%\baidunetdisk 内置浏览器的数据(网页登录状态、缓存等)
# %APPDATA%\BaiduYunGuanjia 客户端数据与日志
# 链接每次启动时检查并补建(例如 C 盘还原后链接会消失);链接目标里的 .portable-link 文件记着目标路径,用来确认链接指向的正是这里。
# 该位置已是普通文件夹时不覆盖,提示用户处理后再启动。
$launcherText = @'
@echo off
setlocal
set "PORTABLE_DIR=%~dp0"
call :link "%APPDATA%\baidunetdisk" "%PORTABLE_DIR%Data\AppData\Roaming\baidunetdisk" || goto :fail
call :link "%APPDATA%\BaiduYunGuanjia" "%PORTABLE_DIR%Data\AppData\Roaming\BaiduYunGuanjia" || goto :fail
if not exist "%PORTABLE_DIR%Data\users" md "%PORTABLE_DIR%Data\users"
start "" "%PORTABLE_DIR%App\BaiduNetdisk.exe" %*
exit /b
:link
if not exist "%~2" md "%~2"
> "%~2\.portable-link" echo %~2
call :linked "%~1" "%~2" && exit /b 0
rmdir "%~1" >nul 2>&1
mklink /J "%~1" "%~2" >nul 2>&1
call :linked "%~1" "%~2" && exit /b 0
echo "%~1" is a normal folder, so its data would not stay in the portable folder.
echo Move or delete that folder, then start again.
exit /b 1
:linked
set "MARK="
if exist "%~1\.portable-link" set /p MARK=<"%~1\.portable-link"
if /i "%MARK%"=="%~2" exit /b 0
exit /b 1
:fail
pause
exit /b 1
'@
Save-Text "BaiduNetdiskPortable.bat" $launcherText $utf8NoBom
# 1.2 更新入口
$updateBatText = @'
@echo off
title BaiduNetdisk Portable Update
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0BaiduNetdiskPortableUpdate.ps1"
pause
'@
Save-Text "BaiduNetdiskPortableUpdate.bat" $updateBatText $utf8NoBom
# 1.3 安装/更新脚本
$updatePsText = @'
# 百度网盘便携版安装/更新脚本(由 BaiduNetdiskPortableSetup.bat 生成)
# 无参数 Data\Packages 里有更高版本的安装包时更新
# -Reinstall 即使已是最新也重新部署
# -ResetData 清空 Data(保留安装包)后重新部署
# 安装包由用户放入 Data\Packages(官方完整安装包,文件名不限):使用其中版本最高、带百度签名的一个,部署成功后删除其余安装包。
# 程序文件用 7-Zip 从 NSIS 安装包中直接解出,不运行安装程序;部署后关闭程序自带的自动更新。
# 每次部署都在 App\.portable-manifest 记下安装包里的文件;再次部署时,App 里不在清单上的文件(程序运行中产生的)原样保留。
param([switch]$Reinstall, [switch]$ResetData)
$ErrorActionPreference = "Stop"
$TargetRoot = "D:\Green\BaiduNetdisk"
$TargetApp = Join-Path $TargetRoot "App"
$DataDir = Join-Path $TargetRoot "Data"
$PackageDir = Join-Path $DataDir "Packages"
$Stage = Join-Path $TargetRoot "_unpack"
$Manifest = Join-Path $TargetApp ".portable-manifest"
$MainExe = "BaiduNetdisk.exe"
$Publisher = 'CN="Beijing Duyou Science and Technology Co.,Ltd."*'
$SevenZip = "D:\Green\7-Zip\7z.exe"
# 列出目录下全部文件的相对路径,不进入目录链接
function Get-RelativeFiles($Root) {
$files = New-Object System.Collections.Generic.List[string]
$dirs = New-Object System.Collections.Generic.Stack[string]
$dirs.Push($Root)
while ($dirs.Count) {
foreach ($item in [System.IO.Directory]::GetFileSystemEntries($dirs.Pop())) {
$attributes = [System.IO.File]::GetAttributes($item)
if (-not ($attributes -band [System.IO.FileAttributes]::Directory)) { $files.Add($item.Substring($Root.Length + 1)) }
elseif (-not ($attributes -band [System.IO.FileAttributes]::ReparsePoint)) { $dirs.Push($item) }
}
}
$files
}
# 1. 本地已安装版本
$localVer = $null
$localExe = Join-Path $TargetApp $MainExe
if ((Test-Path $localExe) -and ((Get-Item $localExe).VersionInfo.FileVersion -match '(\d+(\.\d+)+)')) { $localVer = [version]$matches[1] }
# 2. 在 Data\Packages 中选出版本最高、签名正确的安装包
Write-Host "[*] 正在检查 Data\Packages 中的安装包..." -ForegroundColor Cyan
$package = $null
$targetVer = $null
foreach ($file in Get-ChildItem $PackageDir -Filter "*.exe" -File -ErrorAction SilentlyContinue) {
if ($file.VersionInfo.FileVersion -notmatch '(\d+(\.\d+)+)') { continue }
$version = [version]$matches[1]
if ($targetVer -and $version -le $targetVer) { continue }
$signature = Get-AuthenticodeSignature $file.FullName
if ($signature.Status -ne "Valid" -or $signature.SignerCertificate.Subject -notlike $Publisher) {
Write-Host "[!] 跳过签名不符的文件: $($file.Name)" -ForegroundColor Yellow
continue
}
$package = $file
$targetVer = $version
}
if (-not $package) { throw "Data\Packages 里没有可用的安装包,请放入百度网盘官方完整安装包后重试。" }
Write-Host "本地版本: $(if ($localVer) { $localVer } else { '未安装' })" -ForegroundColor Yellow
Write-Host "安装包版本: $targetVer($($package.Name))" -ForegroundColor Yellow
if (-not ($Reinstall -or $ResetData) -and $localVer -and $localVer -ge $targetVer) {
Write-Host "`n[OK] 当前已是最新版本,无需更新!" -ForegroundColor Green
return
}
if (-not (Test-Path $SevenZip)) { throw "未找到 7-Zip:$SevenZip" }
if (Test-Path $Stage) { Remove-Item $Stage -Recurse -Force }
try {
# 3. 把程序文件解到临时目录,确认完整后才改动现有程序
Write-Host "`n[*] 正在解出程序文件..." -ForegroundColor Cyan
& $SevenZip x -tNsis -y -bso0 -bsp0 "-o$Stage" $package.FullName
if ($LASTEXITCODE) { throw "解包失败(7-Zip 返回 $LASTEXITCODE),安装包可能已损坏。" }
foreach ($name in '$PLUGINSDIR', 'uninst.exe') {
$path = Join-Path $Stage $name
if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force }
}
$stageExe = Join-Path $Stage $MainExe
if (-not (Test-Path $stageExe) -or (Get-AuthenticodeSignature $stageExe).Status -ne "Valid") { throw "安装包里没有有效的 $MainExe,可能不是完整安装包。" }
$shipped = @(Get-RelativeFiles $Stage)
# 4. 结束本便携目录下运行的所有进程,并等待退出
# 进程在退出过程中可能再启动子进程,因此反复结束,直到没有剩余或超过 15 秒
Write-Host "[*] 正在终止运行中的百度网盘进程..." -ForegroundColor Yellow
$inTarget = { $_.Path -and $_.Path.StartsWith("$TargetRoot\", [StringComparison]::OrdinalIgnoreCase) }
$deadline = (Get-Date).AddSeconds(15)
do {
$procs = @(Get-Process | Where-Object $inTarget)
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
$procs | Wait-Process -Timeout 5 -ErrorAction SilentlyContinue
} while ($procs.Count -and (Get-Date) -lt $deadline)
$left = @(Get-Process | Where-Object $inTarget)
if ($left.Count) { throw ("以下进程仍未退出,请手动关闭后重试: " + (($left | ForEach-Object { $_.Path } | Sort-Object -Unique) -join ", ")) }
# 5. 清空数据;或把程序运行中在 App 里产生的文件移到新程序目录
if ($ResetData) {
Write-Host "[*] 正在清空数据(保留安装包)..." -ForegroundColor Yellow
Get-ChildItem $DataDir -Force | Where-Object { $_.FullName -ne $PackageDir } | Remove-Item -Recurse -Force
} elseif (Test-Path $Manifest) {
$known = New-Object System.Collections.Generic.HashSet[string]([StringComparer]::OrdinalIgnoreCase)
foreach ($line in [System.IO.File]::ReadAllLines($Manifest)) { [void]$known.Add($line) }
$kept = 0
foreach ($relative in @(Get-RelativeFiles $TargetApp)) {
if ($relative -eq ".portable-manifest" -or $known.Contains($relative)) { continue }
$destination = Join-Path $Stage $relative
if (Test-Path -LiteralPath $destination) { continue }
[System.IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) | Out-Null
Move-Item -LiteralPath (Join-Path $TargetApp $relative) -Destination $destination
$kept++
}
Write-Host "[*] 保留了 $kept 个程序运行中产生的文件。" -ForegroundColor Cyan
}
# 6. 换上新的程序目录(先单独移除目录链接,确保不会删到链接指向的数据)
if (Test-Path $TargetApp) {
Get-ChildItem $TargetApp -Force -Directory | Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint } | ForEach-Object { $_.Delete() }
Remove-Item $TargetApp -Recurse -Force
}
Move-Item $Stage $TargetApp
} finally {
if (Test-Path $Stage) { Remove-Item $Stage -Recurse -Force }
}
# 7. 用户数据目录 users 链接到 Data\users,关闭自带的自动更新,记下安装包里的文件清单
$usersData = Join-Path $DataDir "users"
New-Item -ItemType Directory -Path $usersData -Force | Out-Null
New-Item -ItemType Junction -Path (Join-Path $TargetApp "users") -Value $usersData | Out-Null
$updateConfig = Join-Path $TargetApp "AutoUpdate\config.ini"
if (Test-Path $updateConfig) {
$text = [System.IO.File]::ReadAllText($updateConfig, [System.Text.Encoding]::Default)
[System.IO.File]::WriteAllText($updateConfig, [regex]::Replace($text, '(?m)^(IsAutoUpdate=)\d+', '${1}0'), [System.Text.Encoding]::Default)
}
[System.IO.File]::WriteAllLines($Manifest, [string[]]$shipped, (New-Object System.Text.UTF8Encoding($false)))
# 8. 删除其余旧安装包
Get-ChildItem $PackageDir -Filter "*.exe" -File | Where-Object { $_.FullName -ne $package.FullName } | ForEach-Object {
Remove-Item $_.FullName -Force
Write-Host "[*] 已删除旧安装包: $($_.Name)" -ForegroundColor Cyan
}
Write-Host "`n[OK] 百度网盘 $targetVer 部署完成!" -ForegroundColor Green
'@
Save-Text "BaiduNetdiskPortableUpdate.ps1" $updatePsText $utf8WithBom
Write-Host "[*] 启动器与更新组件写入完成。" -ForegroundColor Green
# 2. 部署核心程序
Write-Host "`n[2/3] 部署核心程序..." -ForegroundColor Cyan
& $UpdatePs1 @deployArgs
# 3. 生成桌面快捷方式
Write-Host "`n[3/3] 刷新桌面快捷方式..." -ForegroundColor Cyan
$ws = New-Object -ComObject WScript.Shell
$desktop = [Environment]::GetFolderPath('Desktop')
$shortcuts = [ordered]@{ "BaiduNetdisk Portable" = "BaiduNetdiskPortable.bat"; "BaiduNetdisk Update" = "BaiduNetdiskPortableUpdate.bat" }
foreach ($name in $shortcuts.Keys) {
$lnk = $ws.CreateShortcut((Join-Path $desktop "$name.lnk"))
$lnk.TargetPath = Join-Path $Target $shortcuts[$name]
$lnk.WorkingDirectory = $Target
$lnk.IconLocation = "$TargetApp\BaiduNetdisk.exe,0"
$lnk.Save()
Write-Host "[*] 已生成快捷方式: $name.lnk" -ForegroundColor Green
}
Write-Host "`n======================================================" -ForegroundColor Green
Write-Host "部署完成!全套绿色组件已就绪。" -ForegroundColor Green
Write-Host "======================================================" -ForegroundColor GreenQuarkPortableSetup.bat
@echo off
:: Runs the PowerShell section below. Every batch line must stay ASCII-only (cmd misparses Chinese text).
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$lines = [System.IO.File]::ReadAllLines('%~f0', [System.Text.Encoding]::UTF8);" ^
"$start = [Array]::IndexOf($lines, ':::POWERSHELL_BEGIN:::') + 1;" ^
"Invoke-Expression ($lines[$start..($lines.Length - 1)] -join [Environment]::NewLine)"
pause
exit /b
:::POWERSHELL_BEGIN:::
$ErrorActionPreference = "Stop"
$Target = "D:\Green\Quark"
$TargetApp = Join-Path $Target "App"
$UpdatePs1 = Join-Path $Target "QuarkPortableUpdate.ps1"
# 生成文件的编码:
# .bat 无 BOM(否则 cmd 无法识别首行),且只含 ASCII 字符,中文提示一律由 PowerShell 输出
# .ps1 带 BOM(否则 Windows PowerShell 5.1 会按 ANSI 编码读取中文)
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$utf8WithBom = New-Object System.Text.UTF8Encoding($true)
# 以 Windows 换行符写出生成的文件
function Save-Text($Name, $Text, $Encoding) {
[System.IO.File]::WriteAllText((Join-Path $Target $Name), (($Text -replace "`r?`n", "`r`n") + "`r`n"), $Encoding)
}
Write-Host "======================================================" -ForegroundColor Cyan
Write-Host " 夸克网盘 绿色便携化自动部署向导 " -ForegroundColor Cyan
Write-Host "======================================================" -ForegroundColor Cyan
# 部署方式:默认重新部署;已安装时可改选彻底重置或退出(部署由 QuarkPortableUpdate.ps1 完成)
$deployArgs = @{ Reinstall = $true }
if (Test-Path "$TargetApp\quark_cloud_drive.exe") {
Write-Host "`n[提示] 检测到已存在安装版本。" -ForegroundColor Yellow
Write-Host " [1] 重新部署应用核心(保留登录状态与个人配置)"
Write-Host " [2] 彻底全新重置(清空所有数据,仅保留安装包)"
Write-Host " [3] 退出"
$choice = ""
while ($choice -notmatch '^[123]$') { $choice = (Read-Host "`n请选择操作 [1, 2, 3]").Trim() }
if ($choice -eq "3") { Write-Host "已取消操作。" -ForegroundColor Gray; return }
if ($choice -eq "2") { $deployArgs.ResetData = $true }
}
# 1. 写入启动器与更新组件
Write-Host "`n[1/3] 写入启动器与更新组件..." -ForegroundColor Cyan
# 1.1 启动器:夸克网盘的部分模块不认 --user-data-dir,总是写入默认位置,因此把它在 LOCALAPPDATA 下的数据目录链接到 Data 里
# %LOCALAPPDATA%\QuarkCloudDrive 主程序数据(登录、设置、缓存等)
# %LOCALAPPDATA%\CloudDriveDesktopWidgetIndependent 桌面小组件的设置
# %LOCALAPPDATA%\com.quark.clouddrive.desktopwidget.independent 桌面小组件的内置浏览器数据
# 链接每次启动时检查并补建(例如 C 盘还原后链接会消失);链接目标里的 .portable-link 文件记着目标路径,用来确认链接指向的正是这里。
# 该位置已是普通文件夹时不覆盖,提示用户处理后再启动。
$launcherText = @'
@echo off
setlocal
set "PORTABLE_DIR=%~dp0"
call :link "%LOCALAPPDATA%\QuarkCloudDrive" "%PORTABLE_DIR%Data\AppData\Local\QuarkCloudDrive" || goto :fail
call :link "%LOCALAPPDATA%\CloudDriveDesktopWidgetIndependent" "%PORTABLE_DIR%Data\AppData\Local\CloudDriveDesktopWidgetIndependent" || goto :fail
call :link "%LOCALAPPDATA%\com.quark.clouddrive.desktopwidget.independent" "%PORTABLE_DIR%Data\AppData\Local\com.quark.clouddrive.desktopwidget.independent" || goto :fail
start "" "%PORTABLE_DIR%App\quark_cloud_drive.exe" %*
exit /b
:link
if not exist "%~2" md "%~2"
> "%~2\.portable-link" echo %~2
call :linked "%~1" "%~2" && exit /b 0
rmdir "%~1" >nul 2>&1
mklink /J "%~1" "%~2" >nul 2>&1
call :linked "%~1" "%~2" && exit /b 0
echo "%~1" is a normal folder, so its data would not stay in the portable folder.
echo Move or delete that folder, then start again.
exit /b 1
:linked
set "MARK="
if exist "%~1\.portable-link" set /p MARK=<"%~1\.portable-link"
if /i "%MARK%"=="%~2" exit /b 0
exit /b 1
:fail
pause
exit /b 1
'@
Save-Text "QuarkPortable.bat" $launcherText $utf8NoBom
# 1.2 更新入口
$updateBatText = @'
@echo off
title Quark Portable Update
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0QuarkPortableUpdate.ps1"
pause
'@
Save-Text "QuarkPortableUpdate.bat" $updateBatText $utf8NoBom
# 1.3 安装/更新脚本
$updatePsText = @'
# 夸克网盘便携版安装/更新脚本(由 QuarkPortableSetup.bat 生成)
# 无参数 Data\Packages 里有更高版本的安装包时更新
# -Reinstall 即使已是最新也重新部署
# -ResetData 清空 Data(保留安装包)后重新部署
# 安装包由用户放入 Data\Packages(官方完整安装包,文件名不限;在线安装器 releasemini 里没有程序文件,不能用):
# 使用其中版本最高、带阿里巴巴签名的一个,部署成功后删除其余安装包。
# 程序文件用 innoextract 从 Inno Setup 安装包中直接解出,不运行安装程序;部署后关闭程序自带的自动更新。
# 每次部署都在 App\.portable-manifest 记下安装包里的文件;再次部署时,App 里不在清单上的文件(程序运行中产生的)原样保留。
param([switch]$Reinstall, [switch]$ResetData)
$ErrorActionPreference = "Stop"
$TargetRoot = "D:\Green\Quark"
$TargetApp = Join-Path $TargetRoot "App"
$DataDir = Join-Path $TargetRoot "Data"
$PackageDir = Join-Path $DataDir "Packages"
$Stage = Join-Path $TargetRoot "_unpack"
$Manifest = Join-Path $TargetApp ".portable-manifest"
$MainExe = "quark_cloud_drive.exe"
$Publisher = 'CN="ALIBABA (CHINA) NETWORK TECHNOLOGY CO.,LTD."*'
$Innoextract = "D:\Green\innoextract\innoextract.exe"
# 安装程序在安装时才算出的文件名(innoextract 解出时保留为 code$<函数名>)
$ComputedNames = [ordered]@{
'code$GetAppExeDestName' = 'quark_cloud_drive.exe'
'code$GetAppProxyExeDestName' = 'quark_cloud_drive_proxy.exe'
'components\clouddrive_desktop_widget_independent\code$GetAppWidgetExeName' = 'clouddrive_desktop_widget_independent.exe'
}
# 列出目录下全部文件的相对路径,不进入目录链接
function Get-RelativeFiles($Root) {
$files = New-Object System.Collections.Generic.List[string]
$dirs = New-Object System.Collections.Generic.Stack[string]
$dirs.Push($Root)
while ($dirs.Count) {
foreach ($item in [System.IO.Directory]::GetFileSystemEntries($dirs.Pop())) {
$attributes = [System.IO.File]::GetAttributes($item)
if (-not ($attributes -band [System.IO.FileAttributes]::Directory)) { $files.Add($item.Substring($Root.Length + 1)) }
elseif (-not ($attributes -band [System.IO.FileAttributes]::ReparsePoint)) { $dirs.Push($item) }
}
}
$files
}
# 1. 本地已安装版本
$localVer = $null
$localExe = Join-Path $TargetApp $MainExe
if ((Test-Path $localExe) -and ((Get-Item $localExe).VersionInfo.FileVersion -match '(\d+(\.\d+)+)')) { $localVer = [version]$matches[1] }
# 2. 在 Data\Packages 中选出版本最高、签名正确的安装包
Write-Host "[*] 正在检查 Data\Packages 中的安装包..." -ForegroundColor Cyan
$package = $null
$targetVer = $null
foreach ($file in Get-ChildItem $PackageDir -Filter "*.exe" -File -ErrorAction SilentlyContinue) {
if ($file.VersionInfo.FileVersion -notmatch '(\d+(\.\d+)+)') { continue }
$version = [version]$matches[1]
if ($targetVer -and $version -le $targetVer) { continue }
$signature = Get-AuthenticodeSignature $file.FullName
if ($signature.Status -ne "Valid" -or $signature.SignerCertificate.Subject -notlike $Publisher) {
Write-Host "[!] 跳过签名不符的文件: $($file.Name)" -ForegroundColor Yellow
continue
}
$package = $file
$targetVer = $version
}
if (-not $package) { throw "Data\Packages 里没有可用的安装包,请放入夸克网盘官方完整安装包后重试。" }
Write-Host "本地版本: $(if ($localVer) { $localVer } else { '未安装' })" -ForegroundColor Yellow
Write-Host "安装包版本: $targetVer($($package.Name))" -ForegroundColor Yellow
if (-not ($Reinstall -or $ResetData) -and $localVer -and $localVer -ge $targetVer) {
Write-Host "`n[OK] 当前已是最新版本,无需更新!" -ForegroundColor Green
return
}
if (-not (Test-Path $Innoextract)) { throw "未找到 innoextract:$Innoextract" }
if (Test-Path $Stage) { Remove-Item $Stage -Recurse -Force }
try {
# 3. 把程序文件解到临时目录,确认完整后才改动现有程序
Write-Host "`n[*] 正在解出程序文件..." -ForegroundColor Cyan
& $Innoextract -e -s -d $Stage $package.FullName
if ($LASTEXITCODE) { throw "解包失败(innoextract 返回 $LASTEXITCODE),安装包可能已损坏。" }
$program = Join-Path $Stage "app"
foreach ($entry in $ComputedNames.GetEnumerator()) {
$path = Join-Path $program $entry.Key
if (Test-Path -LiteralPath $path) { Rename-Item -LiteralPath $path -NewName $entry.Value }
}
foreach ($unknown in Get-ChildItem $program -Recurse -File -Filter 'code$*' -ErrorAction SilentlyContinue) {
Write-Host "[!] 无法确定文件名,保留原名: $($unknown.FullName.Substring($program.Length + 1))" -ForegroundColor Yellow
}
$stageExe = Join-Path $program $MainExe
if (-not (Test-Path $stageExe) -or (Get-AuthenticodeSignature $stageExe).Status -ne "Valid") { throw "安装包里没有有效的 $MainExe,可能不是完整安装包(在线安装器不能用)。" }
# 程序自带的更新会安装独立的更新服务,删除它的安装程序即可关闭
Get-ChildItem $program -Recurse -File -Filter "QuarkCloudDriveUpdaterSetup.exe" | Remove-Item -Force
$shipped = @(Get-RelativeFiles $program)
# 4. 结束本便携目录下运行的所有进程,并等待退出
# 进程在退出过程中可能再启动子进程,因此反复结束,直到没有剩余或超过 15 秒
Write-Host "[*] 正在终止运行中的夸克网盘进程..." -ForegroundColor Yellow
$inTarget = { $_.Path -and $_.Path.StartsWith("$TargetRoot\", [StringComparison]::OrdinalIgnoreCase) }
$deadline = (Get-Date).AddSeconds(15)
do {
$procs = @(Get-Process | Where-Object $inTarget)
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
$procs | Wait-Process -Timeout 5 -ErrorAction SilentlyContinue
} while ($procs.Count -and (Get-Date) -lt $deadline)
$left = @(Get-Process | Where-Object $inTarget)
if ($left.Count) { throw ("以下进程仍未退出,请手动关闭后重试: " + (($left | ForEach-Object { $_.Path } | Sort-Object -Unique) -join ", ")) }
# 5. 清空数据;或把程序运行中在 App 里产生的文件移到新程序目录
if ($ResetData) {
Write-Host "[*] 正在清空数据(保留安装包)..." -ForegroundColor Yellow
Get-ChildItem $DataDir -Force | Where-Object { $_.FullName -ne $PackageDir } | Remove-Item -Recurse -Force
} elseif (Test-Path $Manifest) {
$known = New-Object System.Collections.Generic.HashSet[string]([StringComparer]::OrdinalIgnoreCase)
foreach ($line in [System.IO.File]::ReadAllLines($Manifest)) { [void]$known.Add($line) }
$kept = 0
foreach ($relative in @(Get-RelativeFiles $TargetApp)) {
if ($relative -eq ".portable-manifest" -or $known.Contains($relative)) { continue }
$destination = Join-Path $program $relative
if (Test-Path -LiteralPath $destination) { continue }
[System.IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) | Out-Null
Move-Item -LiteralPath (Join-Path $TargetApp $relative) -Destination $destination
$kept++
}
Write-Host "[*] 保留了 $kept 个程序运行中产生的文件。" -ForegroundColor Cyan
}
# 6. 换上新的程序目录
if (Test-Path $TargetApp) { Remove-Item $TargetApp -Recurse -Force }
Move-Item $program $TargetApp
} finally {
if (Test-Path $Stage) { Remove-Item $Stage -Recurse -Force }
}
# 7. 记下安装包里的文件清单
[System.IO.File]::WriteAllLines($Manifest, [string[]]$shipped, (New-Object System.Text.UTF8Encoding($false)))
# 8. 删除其余旧安装包
Get-ChildItem $PackageDir -Filter "*.exe" -File | Where-Object { $_.FullName -ne $package.FullName } | ForEach-Object {
Remove-Item $_.FullName -Force
Write-Host "[*] 已删除旧安装包: $($_.Name)" -ForegroundColor Cyan
}
Write-Host "`n[OK] 夸克网盘 $targetVer 部署完成!" -ForegroundColor Green
'@
Save-Text "QuarkPortableUpdate.ps1" $updatePsText $utf8WithBom
Write-Host "[*] 启动器与更新组件写入完成。" -ForegroundColor Green
# 2. 部署核心程序
Write-Host "`n[2/3] 部署核心程序..." -ForegroundColor Cyan
& $UpdatePs1 @deployArgs
# 3. 生成桌面快捷方式
Write-Host "`n[3/3] 刷新桌面快捷方式..." -ForegroundColor Cyan
$ws = New-Object -ComObject WScript.Shell
$desktop = [Environment]::GetFolderPath('Desktop')
$shortcuts = [ordered]@{ "Quark Portable" = "QuarkPortable.bat"; "Quark Update" = "QuarkPortableUpdate.bat" }
foreach ($name in $shortcuts.Keys) {
$lnk = $ws.CreateShortcut((Join-Path $desktop "$name.lnk"))
$lnk.TargetPath = Join-Path $Target $shortcuts[$name]
$lnk.WorkingDirectory = $Target
$lnk.IconLocation = "$TargetApp\quark_cloud_drive.exe,0"
$lnk.Save()
Write-Host "[*] 已生成快捷方式: $name.lnk" -ForegroundColor Green
}
Write-Host "`n======================================================" -ForegroundColor Green
Write-Host "部署完成!全套绿色组件已就绪。" -ForegroundColor Green
Write-Host "======================================================" -ForegroundColor Green