国内外双版本同源客户端隔离助手(win-electron-edition-split)
两个发行版通常是同一套代码打包的,域名与品牌本来就分开,坏在落盘的中间状态共享。所以本 skill 的重心不是"重装",而是"找出共享状态并证明它已闭合"。
不适用
单实例多开、多账号并行、纯粹装不上或报错——那些走各自的排障路径。本 skill 只处理"两个同源客户端互相污染"这一类。
根因模型:五类可能共享的状态
逐项排查,任何一项共享都是嫌疑通道。
| # | 共享项 | 位置 | 判定 | 处置 |
|---|---|---|---|---|
| 1 | 更新缓存目录 | %LOCALAPPDATA%\@<pkgname>-updater |
目录名取自 package name,多数不区分 edition | 必清;新版一般已改为按 applicationName 派生 %TEMP%\<app>-update-<arch>[-<appname>] |
| 2 | Temp 下载目录 | %TEMP%\<product>-update-* |
看是否带 edition 后缀 | 重建出的空目录无害,可留 |
| 3 | 运行时数据目录 | ~\.<product> 与 ~\.<product>-ai、%APPDATA%\<ProductName> |
两版各启动一次后比对文件时间戳 | 若撞同一个目录,查 <PRODUCT>_USER_DATA_DIR 一类环境变量 |
| 4 | 安装目录 | HKCU Uninstall 的 UninstallString/DisplayIcon |
是否都落在默认 AppData\Local\Programs\X |
静默重装到两个固定目录 |
| 5 | 深链协议 | HKCU\Software\Classes\<scheme>\shell\open\command |
共享 scheme(如 buddy-app://)后启动者抢走 |
只能复位并交付复位脚本,无法根除 |
更新器为什么会把 A 的包装进 B:缓存里统一叫 installer.exe,恢复时只比版本号、不比产品身份。于是低版本一侧能从共享目录翻到高版本一侧的包,把对方目录刷成只剩几个文件。日志签名:Restored downloaded update from cache。
工作流
复制这份清单跟踪进度。阶段 1–3 全是只读,必须先做完再动任何破坏性操作。
- [ ] 0 立项确认(目标目录 / 安装包来源 / 残留处置 / 版本 / 绝不能动的数据)
- [ ] 1 勘察:安装、注册表、缓存、协议、快捷方式、运行时目录
- [ ] 2 go/no-go:缓存里那个包的 SHA256 是否等于官方安装包
- [ ] 3 备份:登录数据 + AppData 配置 + 目录/注册表/协议/快捷方式快照
- [ ] 4 关闭两版全部进程(含子进程)
- [ ] 5 清更新缓存(送回收站,不 rm)
- [ ] 6 静默卸载 ×2
- [ ] 7 静默重装到两个固定目录
- [ ] 8 复位快捷方式与深链
- [ ] 9 双侧各启动一次 + 逐项验证 + Markdown 报告
阶段 0:立项确认
用 AskUserQuestion 一次问齐,别自己猜:两个安装目录固定到哪;安装包从官网取还是用户已有;除更新缓存外的残留要不要一并隔离;接受最新版还是必须回滚某一版;并明确点名绝不能触碰的数据(一般是登录态目录)。中途用户改目录是常态,改完立刻全线替换路径再继续。
阶段 1:勘察
跑 scripts/survey.ps1(只读)。它汇总两版的安装位置与版本、产品身份文件、更新缓存内容与 SHA256、HKCU 卸载项、协议归属、快捷方式目标、运行时目录体积。三个判断点:
- 某侧是否已被打坏:安装目录文件数骤减、
resources\app.asar缺失、目录里留着.update-installing.bak且其中记着对方的包名。 - 更新检查域名与平台串:
<runtime>\logs\update\update-YYYYMMDD.log里的Checking for updates: <base>/v2/update?platform=...。 - 身份来源:
resources\install-manifest.json、resources\app.asar.unpacked\...\product.json、可执行文件的VersionInfo.ProductName。
在几十 MB 的 app.asar 里找字符串只能线性 IndexOf 扫,不能上正则,否则灾难性回溯会跑到超时。手法见 PLAYBOOK.md。
阶段 2:go/no-go 校验
清缓存前必做:算出缓存里 installer.exe 的 SHA256,与官方安装包比对。相等说明删缓存无损(可重取);不等或算不出来,先把它备份走再删。
阶段 3:备份
至少四样,落到同一个 _backup_pre_split 目录:登录数据目录、%APPDATA%\<ProductName> 配置目录、安装目录递归清单、注册表卸载项 + 协议 + 快捷方式快照。复制用 robocopy /E /COPY:DAT /R:1 /W:1,退出码 0–7 都算成功。路径超 260 字符会漏文件,别把枚举差异误判成数据丢失(见 PITFALLS.md)。
阶段 4–5:关进程与清缓存
先优雅关窗,再 Stop-Process 兜底,确认两版所有子进程(sandbox、helper、supervisor 之类)归零。删除一律走回收站,不用 rm/del:
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory($path,'OnlyErrorDialogs','SendToRecycleBin')
删完立刻 Test-Path 验证,并在阶段 9 复查它有没有被重建。
阶段 6–7:静默卸载与重装
# 卸载:从注册表取 UninstallString,per-user 包加 /currentuser
Start-Process -FilePath $uninstaller -ArgumentList @('/currentuser','/S') -PassThru -Wait -WindowStyle Hidden
# 重装到固定目录:/D= 必须是最后一个参数,且路径不加引号
Start-Process -FilePath $installer -ArgumentList @('/S','/D=F:\software\WB\CN') -PassThru -Wait -WindowStyle Hidden
三条铁律:/D=<path> 必须放最后;不能加引号;自定义 helper 的参数名不要叫 $args(与 PowerShell 自动变量冲突,-ArgumentList 会被传成空,卸载/安装看似跑了其实什么都没做)。每个 Start-Process 都要读 ExitCode。
阶段 8:复位
快捷方式用 WScript.Shell COM 读 TargetPath/WorkingDirectory/IconLocation,桌面与开始菜单逐个改到正确目录。深链写 HKCU\Software\Classes\<scheme>\shell\open\command 默认值为 "<exe>" "%1"。专属 scheme 各归各主;共享 scheme 只能指定一边,并把 scripts/fix-protocols.ps1 交付给用户日后复位。
阶段 9:验证
两版各启动一次后逐项取证。不要用注入 <PRODUCT>_UPDATE_URL 的方式来"验证域名"——那是循环论证,只证明了你自己的注入。要读程序自己写下的日志与审计流水。
验证面
| 断言 | 证据 | 通过标准 |
|---|---|---|
| 各查各的域名 | <runtime>\logs\update\update-<今日>.log 的 Checking for updates |
每侧只出现自己的域名 |
| 无跨域名调用 | <runtime>\logs\network-failover.jsonl 今日行的主机集合 |
两侧主机集合不相交 |
| 目录不再互塞 | 两安装目录递归匹配对方特征名;最新文件时间戳 | 异物 0 个,运行后无新增 |
| 安装位置 | HKCU Uninstall 的 UninstallString/DisplayIcon;installer.log 的 final install directory |
指向指定目录 |
| 快捷方式与深链 | .lnk 三项 + Software\Classes\* |
归属正确 |
| 登录态未丢 | 两侧 account-snapshot.json(或等价)+ 渲染日志 isAuthenticated=true + 更新请求自带 x-user-id |
快照为重装后新写入且 uid 不同;登录目录文件数不变 |
| 通道已闭合 | 共享更新缓存是否被重建 | 双侧各启动后仍不存在 |
应用内「环境诊断/自检」面板若无桌面自动化通道,别硬点;它展示的 Install directory / User data directory 可从 installer.log、main.log 里 Self-check … reportDir=、以及实际写入的 userData 目录取同值,并在报告里写明是等价取证而非面板读数。
用户点名的接口若其实不存在(例如 /api/accounts),用 curl 直接取证并给出真实路径:真实路由往往是 /console/accounts,未带凭证返回 302,不存在的路径返回网关 404。需要登录态的接口无法外部调用时,改用上表末行的三条等价证据链。
硬性约束
绝不碰用户点名的登录/账号目录;不永久删除,一律回收站并验证到达;改动前先备份并告知备份位置;每次破坏性动作前必须有只读勘察与 go/no-go。
输出
Markdown 分段报告:结论 → 现在的布局(表) → 逐步骤交代 → 验证证据(含日志原文) → 根因 → 残留风险与维护建议 → 交付物。用户一般偏好 Markdown 而非 HTML 仪表盘。随报告交付 verify-split.ps1(只读体检,逐项 PASS/FAIL)和 fix-protocols.ps1(复位)。
参考资料
- 挖取手法、日志签名、已验证案例:PLAYBOOK.md
- 踩过的坑与修法(PowerShell / 工具层):PITFALLS.md
脚本
都在 scripts/,只读优先、破坏性动作需显式开关,参数化到任意一对 edition(-DirA/-DirB 必填,其余可选——留空的那项检查自动 SKIP,不影响总判定)。
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/survey.ps1 -Match 'WorkBuddy'
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-split.ps1 -DirA 'F:\software\WB\CN' -ExeA 'WorkBuddy.exe' -RuntimeA '.workbuddy' -DomainA 'workbuddy.cn' -SchemeA 'workbuddy' -DirB 'F:\software\WB\AI' -ExeB 'WorkBuddyAI.exe' -RuntimeB '.workbuddy-ai' -DomainB 'workbuddy.ai' -SchemeB 'workbuddy-ai' -Keyword 'WorkBuddy' -SharedScheme 'buddy-app' -SharedTo A -SharedCache '@genieworkbuddy-desktop-updater' -LoginData 'CodeBuddyExtension'
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/fix-protocols.ps1 -DirA 'F:\software\WB\CN' -DirB 'F:\software\WB\AI' -ExeA 'WorkBuddy.exe' -ExeB 'WorkBuddyAI.exe' -SchemeA 'workbuddy' -SchemeB 'workbuddy-ai' -SharedScheme 'buddy-app' -SharedTo A -SharedCache '@genieworkbuddy-desktop-updater' -ClearSharedCache
-ExeA/-ExeB 省略时自动取各目录根最大的 exe;-RuntimeA/-RuntimeB 可写相对 $env:USERPROFILE 的名字(如 .workbuddy)或绝对路径;-SharedCache/-LoginData 写相对 %LOCALAPPDATA% 的目录名。
脚本一律用 -File 执行(不要拼进 -Command),且含中文的 .ps1 必须存成 UTF-8 带 BOM,否则 Windows PowerShell 5.1 按 GBK 解析直接语法报错。