黄老师

我用Claude绿色化百度网盘和夸克
今天接着用 Claude 把百度网盘和夸克网盘也做成了绿色版,分别放在 D:GreenBaiduNetdisk 和...
扫描右侧二维码阅读全文
09
2026/11

我用Claude绿色化百度网盘和夸克

今天接着用 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 DesktopElectronMSIX.NET 的 zip 读取--user-data-dir + CLAUDE_CONFIG_DIR便携运行时未发现自带更新自动下载
DeepSeek HarnessElectronNSIS,内嵌 7z切出 7z,用系统自带的 tar.exe--user-data-dir + DSH_HOME、DSH_AGENTS_HOME删除 app-update.yml自动下载
百度网盘传统客户端 + 内嵌 ChromiumNSIS,LZMA 整体压缩7-Zip目录联接(AppData 两个目录、App\users)IsAutoUpdate=0手动放安装包
夸克网盘定制的 ChromiumInno Setup 6.1innoextract目录联接(LocalAppData 三个目录)删除更新器的安装程序手动放安装包

自动下载的两个软件有官方的版本来源,脚本不用下载就能知道有没有新版本。手动的两个由你把完整安装包放进 Data\Packages,脚本选用其中版本最高、签名正确的一个。

难度从哪来

百度网盘和夸克花的时间是前两个的几倍,大部分来自软件本身,小部分来自制作过程中的失误。

方面Claude / DeepSeek百度网盘 / 夸克
安装包MSIX 就是 zip;DeepSeek 内嵌的 7z 用系统自带的 tar 就能解NSIS 的 LZMA 整体压缩、Inno Setup 都要外部工具;夸克还先拿到一个没有程序的在线安装器
数据位置在程序里一搜就有专用的环境变量没有可用的开关,只能实际运行一次、对比前后快照找出来;夸克的 --user-data-dir 只管一部分数据
程序目录只读百度网盘会在 App 里写设置文件和账号数据,更新时要保留
写入系统很少夸克每次启动都写大量注册表项,每轮测试后都要清理
文件名安装包里就是真名夸克有 3 个文件名安装时才算出来,要从程序代码里查

失误造成的返工:

  • 快照脚本通配符写错(-like '[*')导致卡住,又递归扫描整个用户目录,一次用了 20 多分钟。
  • 测试目录路径太长,超过 260 字符,夸克第一次解包失败。
  • “保留运行中产生的文件”最初一个都没保留(PowerShell 函数返回列表的坑),重新部署会清掉百度网盘的设置。
  • 链接检查最初只看标记是否存在,链接指向旧位置时会误判为有效。
  • 测试后忘了关掉夸克,文件被占用,清理只好重做一次。

这些经验已经写进 GreenCreator 的工具和说明里。

做一个新软件的步骤

从拿到安装包到交付,按下面八步做;前三步只是了解软件,不用写代码。

  1. 拿到完整安装包,核对签名。 用 Get-AuthenticodeSignature 看签名是否有效、签名者是谁。只有几 MB 的通常是在线安装器。
  2. 认出安装包格式,把程序解到测试目录。 不运行安装程序:它会写注册表、把程序装进系统目录。
  3. 查清数据位置。 先在程序文件里搜,再用 Probe-App.ps1 实际运行一次核对,还要看程序会不会往自己的目录里写文件。
  4. 选定把数据放进 Data 的办法(见下一节)。
  5. 找到关闭自带更新的办法。 优先用软件自己的配置项或控制更新的文件。
  6. 写 Setup。 从最接近的现有 Setup 复制一份改:自动下载的参照 Claude 或 DeepSeek Harness,手动放安装包的参照百度网盘或夸克。
  7. 在测试目录里测试(见“测试与清理”)。
  8. 清理测试残留,再在真实目录运行 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 的办法

按下面的顺序选,前一种用不了再用后一种;无论哪种,都只管软件自己的数据。

  1. 软件自己的参数,如 Electron 和 Chromium 的 --user-data-dir。最干净,但夸克说明它不一定管全部数据,要实测。
  2. 软件自己的环境变量,如 CLAUDE_CONFIG_DIR、DSH_HOME。启动器用 setlocal 设置,不会泄漏到别处。
  3. 目录联接:启动器每次启动时,把软件在 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\ClaudeClaudePortableSetup.bat
D:\Green\DeepSeek HarnessDeepSeekHarnessPortableSetup.bat
D:\Green\BaiduNetdiskBaiduNetdiskPortableSetup.bat,DownloadURL.txt(完整安装包下载地址)
D:\Green\QuarkQuarkPortableSetup.bat,DownloadURL.txt
D:\Green\GreenCreatorProbe-App.ps1(找数据位置)、Test-Setup.ps1(真实窗口测试)、Find-Leftovers.ps1(查残留)、README.md
D:\Green\7-Zip7-Zip 26.06,解 NSIS
D:\Green\innoextractinnoextract 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 Green

QuarkPortableSetup.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
Last modification:November 9th, 2026 at 12:43 pm

Leave a Comment