diff --git a/.agents/skills/dds-to-skill/SKILL.md b/.agents/skills/dds-to-skill/SKILL.md index 13ee3fe..064fa7d 100644 --- a/.agents/skills/dds-to-skill/SKILL.md +++ b/.agents/skills/dds-to-skill/SKILL.md @@ -3,7 +3,7 @@ name: dds-to-skill description: > 将单模块 DDS(详细设计说明书)/ PRD / 架构文档转换为一个 All-in-One 全栈开发指导 Skill(Converts a single-module DDS/PRD/Architecture doc into one All-in-One development guidebook Skill)。 输出唯一的 developing- Skill,涵盖 API、数据库、状态机、事件、安全等全栈设计细节抽取与 reference 分层索引。 - 触发场景 Trigger: 当用户需要将单模块 DDS 文档转为可落地的开发指导 Skill / 需要从架构设计文档生成 All-in-One 开发向导。 + 触发场景 Trigger: 当用户需要将单模块 DDS / PRD / 架构文档文档转为可落地的开发指导 Skill / 需要从架构设计文档生成 All-in-One 开发向导。 关键词 Keywords: DDS, PRD, 架构说明, 设计文档, skill 生成, all-in-one, agent skill, reference 抽取, API, 状态机, 事件, Schema。 argument-hint: " [--output-dir ] [--module-name ]" allowed-tools: @@ -15,11 +15,11 @@ allowed-tools: - Bash --- -# DDS-to-Skill:从设计文档生成 All-in-One 开发指导 Skill +# PRD-to-Skill:从设计文档生成 All-in-One 开发指导 Skill 本 Skill 指导你将一份单模块 DDS(Detailed Design Specification)或 PRD / 架构说明文档,转换为**唯一一个**包含全栈开发细节的 `developing-` Skill。 -> **核心理念**:一个 DDS 输入 → 一个 Skill 输出。生成的不是"空洞的工作流提示词",而是**绑定了 DDS 设计细节**、能指导真实全栈开发的 All-in-One 指导书。 +> **核心理念**:一个 PRD 输入 → 一个 Skill 输出。生成**绑定了 PRD 设计细节**、能指导真实全栈开发的 All-in-One 指导书。 > **⚠️ 强制约束**: > - **禁止**生成系统级 Skill(`*-system`) @@ -28,7 +28,7 @@ allowed-tools: --- -## Phase 0:读取与理解 DDS +## Phase 0:读取与理解 PRD ### 0.1 动态注入读取(必须执行) diff --git a/19-CMII快文规范/成研究-快文prompt.md b/19-CMII快文规范/成研究-快文prompt.md index 53eb77c..3bfca58 100644 --- a/19-CMII快文规范/成研究-快文prompt.md +++ b/19-CMII快文规范/成研究-快文prompt.md @@ -6,9 +6,9 @@ 【我的身份与组织信息】 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -- 发文人身份:交付部署特战队 代理组长 +- 发文人身份:交付部署组 技术交付小组 - 发文人姓名:{你的姓名} -- 所在部门: +- 所在部门:低空经济技术研发和运营中心 - 小组规模:4人(含组长) - 小组职责: 1. 负责中移凌云各平台(飞行服务平台、监管平台、AI及视频流媒体平台等)的本地化交付部署工作 @@ -34,7 +34,7 @@ 1. **标题格式**: - 使用"关于……的通知/说明/函"格式 - 标题简洁明了,不超过30字 - - 示例:"关于交付部署特战队与研发团队协同工作的规范说明" + - 示例:"关于交付部署组与研发团队协同工作的规范说明" 2. **正文结构**: - 开头:简要说明发文目的和背景(1-2句话) @@ -44,7 +44,7 @@ 3. **段落与排版**: - 每条要求单独成段,条理清晰 - 重要流程使用编号列表 - - 职责界面使用分组对照形式(如"特战队职责"vs"研发团队职责") + - 职责界面使用分组对照形式(如"交付部署组职责"vs"研发团队职责") ## 二、语气与用词规范(核心重点) @@ -57,7 +57,7 @@ | ❌ 避免使用 | ✅ 推荐使用 | 说明 | |---|---|---| -| 必须、务必 | 应当、请、建议 | 特战队作为4人小组,对10+人团队发文,应避免强硬措辞 | +| 必须、务必 | 应当、请、建议 | 交付部署组作为4人小组,对10+人团队发文,应避免强硬措辞 | | 要求你们 | 请各团队配合 | 体现平等协作而非上下级关系 | | 不得、禁止 | 原则上不宜、建议避免 | 保留弹性空间 | | 我组决定 | 经研究/协商,拟定以下方案 | 体现集体决策,减少个人色彩 | @@ -74,7 +74,7 @@ - "为助力中移凌云市场化拓展工作的高效推进,现就XX工作提出如下协作方案。" **主体句式:** -- "原则上由特战队承担XX工作,XX团队配合完成。" +- "原则上由交付部署组承担XX工作,XX团队配合完成。" - "请XX团队在XX时间节点前,按照以下要求准备相关资料。" - "建议各团队按照以下流程开展工作,以确保协作顺畅。" - "XX团队应当/宜/请……"(表示建议性要求) @@ -88,8 +88,8 @@ 1. **身份敏感性**: - 作为4人小组的代理组长,对10+人团队发文时,语气要特别注意"协商"而非"指挥" - - 多用"我队""特战队"的正式称谓,少用"我""我们"等口语化表达 - - 职责界面划分时,强调"特战队主动承担主要工作",而非单方面给对方增加任务 + - 多用"交付部署组"的正式称谓,少用"我""我们"等口语化表达 + - 职责界面划分时,强调"交付部署组主动承担主要工作",而非单方面给对方增加任务 2. **专业术语使用**: - 使用中移凌云内部通用术语(如"本地化部署""交付物""信创适配""市场化拓展"等) @@ -97,14 +97,14 @@ - 首次出现的缩写或专业名词应提供全称说明 3. **自称与对称规范**: - - 自称:交付部署特战队(简称"特战队",首次出现时给出全称) + - 自称:交付部署组 - 对称研发:XX平台研发团队(简称"研发团队") - 对称行业组:各大区域中心/客户组 4. **简称声明**: - 在正文开头部分统一声明简称定义 - 格式示例: - - "交付部署特战队,下文简称特战队" + - "交付部署组" - "飞行服务平台、监管平台研发团队,下文简称研发团队" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/35-黑苹果DELL/0-安装黑苹果-prompt.md b/35-黑苹果DELL/0-安装黑苹果-prompt.md index 3f68842..b592bde 100644 --- a/35-黑苹果DELL/0-安装黑苹果-prompt.md +++ b/35-黑苹果DELL/0-安装黑苹果-prompt.md @@ -2,7 +2,7 @@ CPU为i7-8565U 内存为16GB 硬盘为512GB fanxiang S500 Pro -网卡为 原装的intel网卡 +网卡为 BCM94360CS2 我因为有大模型的开发需求,想给这台电脑安装黑苹果。我的需求如下: 1. 双系统支持,请保留Windows系统,如果特别麻烦可以不保留 diff --git a/35-黑苹果DELL/1-黑苹果debug/260617-重塑-prompt.md b/35-黑苹果DELL/1-黑苹果debug/260617-重塑-prompt.md new file mode 100644 index 0000000..1600b94 --- /dev/null +++ b/35-黑苹果DELL/1-黑苹果debug/260617-重塑-prompt.md @@ -0,0 +1,23 @@ +我有一台DELL的Latitude 5400 14英寸的笔记本 +CPU为i7-8565U +内存为16GB +硬盘为512GB fanxiang S500 Pro +网卡为 BCM94360CS2 +苹果系统的版本是 Ventura 13.7.8 + +我因为有大模型的开发需求,想给这台电脑安装黑苹果。我的需求如下: +1. 双系统支持,请保留Windows系统,如果特别麻烦可以不保留 +2. 需要codex app 和claude code desktop支持的最低版本的系统 +3. 使用opencore进行引导 + +我已经安装了 https://heipg.cn/macos/macos-ventura-13-7-8-22h730-opencore-106-dev-firpe.html 这个版本的操作系统 + +现在WIFI运行正常,请你帮我完成启动OC的完全优化 F:\EFI\OC +1. OC需要有图形化的页面,支持选择不同的操作系统 +2. OC默认需要启动MacOS +3. 现在声音输出不正常,没有声音输出 +4. 现在触控板的操作支持不全面 没法双指等操作 +5. 系统中 CPU型号显示不正常,现在只显示 4.3Ghz 没有正确的CPU型号 +6. + +黑苹果的优化,需要支持HiDPI,需要禁止休眠睡眠等 \ No newline at end of file diff --git a/35-黑苹果DELL/1-黑苹果debug/260618-黑苹果使用.md b/35-黑苹果DELL/1-黑苹果debug/260618-黑苹果使用.md new file mode 100644 index 0000000..4b43846 --- /dev/null +++ b/35-黑苹果DELL/1-黑苹果debug/260618-黑苹果使用.md @@ -0,0 +1,24 @@ +我有一台DELL的Latitude 5400 14英寸的笔记本 +CPU为i7-8565U +内存为16GB +硬盘为512GB fanxiang S500 Pro +网卡为 BCM94360CS2 +苹果系统的版本是 Ventura 13.7.8 + +你是一名精通黑苹果应用的专家,你需要解决我的如下问题 + +显示问题 +1. 需要开启HiDPI + +休眠、睡眠问题 +1. 屏幕可以息屏,但是不能进入睡眠或者休眠问题 + +EFI启动问题 +1. 我应该如何挂载系统的EFI目录,将现在的U盘中的EFI完全替换到硬盘的EFI目录中? + +远程连接问题 +1. 我从windows应该如何远程访问macos 类似RDP +2. 应该如何将文件传输到macos,U盘 或者 网盘的方式? +3. 使用串流的moonlight+sunshine是否可行 + +请你给出详细的操作步骤 diff --git a/35-黑苹果DELL/5-工作主机迁移.md b/35-黑苹果DELL/2-工作电脑备份/5-工作主机迁移.md similarity index 66% rename from 35-黑苹果DELL/5-工作主机迁移.md rename to 35-黑苹果DELL/2-工作电脑备份/5-工作主机迁移.md index 0394802..cd1464c 100644 --- a/35-黑苹果DELL/5-工作主机迁移.md +++ b/35-黑苹果DELL/2-工作电脑备份/5-工作主机迁移.md @@ -17,19 +17,3 @@ 远景需求: 1. 从台式机远程控制苹果macos的方法及工具 2. 寻找windows最好用的终端工具是哪些,我需要经常进行shell连接 - - -你需要从专业的角度根据上述的需求,进行上述内容的完整流程,请分阶段进行 - - - -关于微信数据同步备份,你需要给我更加详细的操作及优雅的方式 - - - -dism /get-wiminfo /wimfile:D:\sources\install.wim - -dism /online /enable-feature /featurename:VirtualMachinePlatform /all /source:D:\sources\install.wim /LimitAccess - -dism /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all ` - /source:D:\sources\install.wim /LimitAccess \ No newline at end of file diff --git a/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份-高级解决方案-实施方案.md b/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份-高级解决方案-实施方案.md new file mode 100644 index 0000000..f77c3de --- /dev/null +++ b/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份-高级解决方案-实施方案.md @@ -0,0 +1,2505 @@ +# 两台 Windows 工作主机数据同步备份实施方案 + +> **执行边界:** 本文档只给出人工实施步骤。当前生成文档的过程不会安装软件、不会修改系统配置、不会启动同步、不会传输任何数据。 + +**目标:** 在笔记本 `wddsh` 与台式机 `wdd` 之间建立可长期运行的双向数据同步体系,覆盖微信数据、Git 项目仓库、开发工具配置,并保留可回滚版本。 + +**总体架构:** 使用 Syncthing 作为唯一持续同步引擎,承担双向、实时、冲突保留和版本留存。使用 rsync 只做微信数据的首次大体量灌入,减少 Syncthing 初次传输 30GB 海量小文件的时间。rsync 通过笔记本 WSL2 发起,SSH 连接台式机 Windows OpenSSH,再用 `wsl rsync` 调用台式机 WSL2 内的远端 rsync。实施过程先备份、再预演、再单项上线,避免一次性开放所有同步面。 + +**技术栈:** Windows 11、Syncthing、两端 WSL2、Windows OpenSSH、rsync、PowerShell、Windows 任务计划程序、JetBrains Settings Sync、VS Code Settings Sync。 + +--- + +## 0. 实施前总览 + +### 0.1 主机与路径 + +| 项目 | 笔记本 | 台式机 | +|------|--------|--------| +| Windows 用户 | `wddsh` | `wdd` | +| 台式机局域网 IP | 不适用 | `192.168.1.194` | +| 微信数据目录 | `C:\Users\wddsh\xwechat_files` | `C:\Users\wdd\xwechat_files` | +| Git 项目目录 | `C:\Users\wddsh\Documents\IdeaProjects` | `C:\Users\wdd\Documents\IdeaProjects` | +| Syncthing Web UI | `http://127.0.0.1:8384` | `http://127.0.0.1:8384` | + +### 0.2 路径修正 + +原有微信 rsync 脚本中的目标路径示例是: + +```bash +REMOTE_DST="/c/Users/wdd/wechat_files/xwechat_files" +``` + +本次实施必须改为用户已确认的台式机路径: + +```bash +REMOTE_DST="/mnt/c/Users/wdd/xwechat_files" +``` + +如果你的 WSL 发行版能访问 `/c/Users/...`,也可以写成: + +```bash +REMOTE_DST="/c/Users/wdd/xwechat_files" +``` + +实施前以 `ls /mnt/c/Users/wdd` 或 `ls /c/Users/wdd` 人工确认实际挂载路径,二选一固定,不要混用。 + +### 0.3 上线顺序 + +1. 做台式机目标目录备份和空间检查。 +2. 配置笔记本 WSL2 SSH 客户端、台式机 WSL2 rsync 运行时、Windows OpenSSH 免密登录。 +3. 关闭两端微信,先做 rsync dry-run。 +4. 执行一次微信 rsync 首次灌入。 +5. 安装并配对两端 Syncthing。 +6. 先上线微信目录同步并观察。 +7. 再上线 Git 项目目录同步并观察。 +8. 最后上线少量开发工具配置同步。 +9. 配置 Syncthing 自启动、版本保留、巡检流程。 + +--- + +## 1. Phase 0:保护现场与回滚准备 + +### Task 1:确认两端不在写入关键数据 + +**操作位置:** 笔记本和台式机都执行。 + +- [ ] **Step 1:退出微信** + +在两台机器上右键系统托盘微信图标,选择退出。 + +- [ ] **Step 2:确认微信进程已退出** + +打开 PowerShell,人工执行: + +```powershell +Get-Process Weixin -ErrorAction SilentlyContinue +``` + +预期结果:没有输出。 +如果仍有输出,先在任务管理器中结束 `Weixin.exe`,再重复检查。 + +- [ ] **Step 3:关闭 IDE 和编辑器** + +关闭 JetBrains IDE、VS Code、Cursor、Trae、终端中运行的开发服务。 + +- [ ] **Step 4:确认 Git 没有正在执行的操作** + +在笔记本的 `C:\Users\wddsh\Documents\IdeaProjects` 下,对正在使用的仓库逐个执行: + +```powershell +git status +``` + +预期结果:没有 rebase、merge、cherry-pick、bisect 进行中。 +如果存在进行中的 Git 操作,先在原机器上完成或中止该操作后再继续。 + +### Task 2:确认台式机磁盘空间 + +**操作位置:** 台式机。 + +- [ ] **Step 1:查看 C 盘可用空间** + +打开 PowerShell,人工执行: + +```powershell +Get-PSDrive C | Select-Object Name,Used,Free +``` + +判断标准: + +- 微信数据约 30GB,至少预留 80GB。 +- Git 项目目录按实际大小再额外预留 30%。 +- Syncthing 版本保留会额外占用空间,首次上线建议 C 盘剩余空间不少于 120GB。 + +- [ ] **Step 2:确认台式机目标目录存在** + +人工执行: + +```powershell +New-Item -ItemType Directory -Force -Path "C:\Users\wdd\xwechat_files" +New-Item -ItemType Directory -Force -Path "C:\Users\wdd\Documents\IdeaProjects" +``` + +预期结果:目录存在;如果命令提示目录已存在,也属于正常。 + +### Task 3:创建台式机本地保护备份 + +**操作位置:** 台式机。 + +- [ ] **Step 1:创建备份根目录** + +人工执行: + +```powershell +New-Item -ItemType Directory -Force -Path "C:\Users\wdd\sync-preflight-backup" +``` + +- [ ] **Step 2:备份现有微信目录** + +如果 `C:\Users\wdd\xwechat_files` 已有数据,人工执行: + +```powershell +robocopy "C:\Users\wdd\xwechat_files" "C:\Users\wdd\sync-preflight-backup\xwechat_files" /E /COPY:DAT /DCOPY:DAT /R:1 /W:1 /XJ +``` + +预期结果:`robocopy` 退出码 `0` 到 `7` 都表示复制层面可接受;`8` 及以上表示有失败项,需要先查看输出。 + +- [ ] **Step 3:备份现有 Git 项目目录** + +如果 `C:\Users\wdd\Documents\IdeaProjects` 已有数据,人工执行: + +```powershell +robocopy "C:\Users\wdd\Documents\IdeaProjects" "C:\Users\wdd\sync-preflight-backup\IdeaProjects" /E /COPY:DAT /DCOPY:DAT /R:1 /W:1 /XJ /XD node_modules .gradle build dist target .idea\system +``` + +预期结果:同样以 `robocopy` 退出码 `0` 到 `7` 为可接受。 + +--- + +## 2. Phase 1:Windows OpenSSH 与两端 WSL2 rsync 准备 + +### Task 4:确认两端 OpenSSH 可用 + +**操作位置:** 笔记本。 + +- [ ] **Step 1:检查 ssh 客户端** + +人工执行: + +```powershell +ssh -V +``` + +预期结果:输出 OpenSSH 版本。 + +- [ ] **Step 2:测试台式机 22 端口连通** + +人工执行: + +```powershell +Test-NetConnection 192.168.1.194 -Port 22 +``` + +预期结果:`TcpTestSucceeded : True`。 + +- [ ] **Step 3:如果端口不通,在台式机启用 OpenSSH Server** + +在台式机以管理员 PowerShell 人工执行: + +```powershell +Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*' +``` + +如果状态不是 `Installed`,再人工执行: + +```powershell +Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 +Start-Service sshd +Set-Service -Name sshd -StartupType Automatic +New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 +``` + +执行后回到笔记本重复 `Test-NetConnection 192.168.1.194 -Port 22`。 + +### Task 5:配置 SSH 免密登录 + +**操作位置:** 笔记本。 + +- [ ] **Step 1:检查本机 SSH 密钥** + +人工执行: + +```powershell +Test-Path "C:\Users\wddsh\.ssh\id_ed25519.pub" +``` + +预期结果:`True`。 + +- [ ] **Step 2:如果没有密钥,创建 ed25519 密钥** + +人工执行: + +```powershell +ssh-keygen -t ed25519 -f "C:\Users\wddsh\.ssh\id_ed25519" -C "wddsh-r9000p-to-wdd-desktop" +``` + +提示 passphrase 时,可以直接回车留空,便于自动化 rsync。 + +- [ ] **Step 3:把公钥追加到台式机** + +优先人工执行: + +```powershell +type "C:\Users\wddsh\.ssh\id_ed25519.pub" | ssh wdd@192.168.1.194 "mkdir .ssh 2>NUL & type con >> .ssh\authorized_keys" +``` + +如果台式机 SSH 进入的是 PowerShell 而不是 CMD,上面命令失败时改用: + +```powershell +$pub = Get-Content "C:\Users\wddsh\.ssh\id_ed25519.pub" -Raw +ssh wdd@192.168.1.194 "powershell -NoProfile -Command `"New-Item -ItemType Directory -Force -Path `$env:USERPROFILE\.ssh | Out-Null; Add-Content -Path `$env:USERPROFILE\.ssh\authorized_keys -Value '$pub'`"" +``` + +- [ ] **Step 4:验证免密 SSH** + +人工执行: + +```powershell +ssh -i "C:\Users\wddsh\.ssh\id_ed25519" wdd@192.168.1.194 "echo SSH_OK" +``` + +预期结果: + +```text +SSH_OK +``` + +### Task 6:在 WSL2 中确认 rsync 可用 + +**操作位置:** 笔记本 WSL2。 + +- [ ] **Step 1:进入 WSL2** + +在笔记本 PowerShell 人工执行: + +```powershell +wsl +``` + +- [ ] **Step 2:确认 rsync 与 ssh 可用** + +在 WSL2 中人工执行: + +```bash +rsync --version +ssh -V +``` + +预期结果:两个命令都输出版本。 + +- [ ] **Step 3:如果 rsync 不存在,在 WSL2 中安装** + +Ubuntu/Debian 系 WSL2 人工执行: + +```bash +sudo apt update +sudo apt install -y rsync openssh-client +``` + +执行后重复: + +```bash +rsync --version +``` + +### Task 6a:在台式机安装 WSL2 并准备远端 rsync + +**操作位置:** 台式机。 +**目的:** rsync 通过 SSH 工作时,远端也必须存在可执行的 `rsync`。本方案不在台式机 WSL2 内单独配置 SSH 服务,而是让 Windows OpenSSH 接收连接后执行 `wsl rsync`。 + +- [ ] **Step 1:检查台式机是否已有 WSL2 发行版** + +在台式机 PowerShell 人工执行: + +```powershell +wsl -l -v +``` + +预期结果:至少有一个发行版,且 `VERSION` 是 `2`。 + +如果看到 `Windows Subsystem for Linux has no installed distributions`,说明还没有安装发行版,继续 Step 2。 + +- [ ] **Step 2:安装 Ubuntu WSL2 发行版** + +在台式机管理员 PowerShell 人工执行: + +```powershell +wsl --install -d Ubuntu +``` + +如果系统提示重启,重启台式机。重启后打开 Ubuntu,按提示创建 Linux 用户名和密码。Linux 用户名可以使用: + +```text +wdd +``` + +- [ ] **Step 3:确认默认 WSL 版本是 2** + +在台式机 PowerShell 人工执行: + +```powershell +wsl --set-default-version 2 +wsl -l -v +``` + +如果 Ubuntu 显示 `VERSION 1`,人工执行: + +```powershell +wsl --set-version Ubuntu 2 +``` + +然后重复: + +```powershell +wsl -l -v +``` + +- [ ] **Step 4:在台式机 WSL2 中安装 rsync 和基础工具** + +在台式机 Ubuntu/WSL2 终端人工执行: + +```bash +sudo apt update +sudo apt install -y rsync openssh-client coreutils +``` + +执行后验证: + +```bash +rsync --version +test -d /mnt/c/Users/wdd && echo DESKTOP_WSL_CAN_ACCESS_WINDOWS_C +``` + +预期结果: + +```text +DESKTOP_WSL_CAN_ACCESS_WINDOWS_C +``` + +- [ ] **Step 5:在台式机 WSL2 中创建微信目标目录** + +在台式机 Ubuntu/WSL2 终端人工执行: + +```bash +mkdir -p /mnt/c/Users/wdd/xwechat_files +test -d /mnt/c/Users/wdd/xwechat_files && echo REMOTE_WECHAT_DIR_OK +``` + +预期结果: + +```text +REMOTE_WECHAT_DIR_OK +``` + +- [ ] **Step 6:从台式机 Windows 侧验证 `wsl rsync` 可被 OpenSSH 调用** + +回到台式机 PowerShell,人工执行: + +```powershell +wsl rsync --version +wsl test -d /mnt/c/Users/wdd/xwechat_files +``` + +预期结果: + +- `wsl rsync --version` 输出 rsync 版本。 +- `wsl test -d ...` 没有报错。 + +- [ ] **Step 7:从笔记本 Windows 侧验证远端 `wsl rsync`** + +在笔记本 PowerShell 人工执行: + +```powershell +ssh -i "C:\Users\wddsh\.ssh\id_ed25519" wdd@192.168.1.194 "wsl rsync --version" +ssh -i "C:\Users\wddsh\.ssh\id_ed25519" wdd@192.168.1.194 "wsl test -d /mnt/c/Users/wdd/xwechat_files && echo REMOTE_RSYNC_READY" +``` + +预期结果: + +```text +REMOTE_RSYNC_READY +``` + +如果第一条命令提示 `wsl: command not found` 或 `rsync: not found`,说明台式机 WSL2 或 rsync 没有配置完成,回到本 Task 的 Step 1 到 Step 6。 + +### Task 7:在 WSL2 中配置 SSH Host 别名 + +**操作位置:** 笔记本 WSL2。 + +- [ ] **Step 1:创建 SSH 配置目录** + +人工执行: + +```bash +mkdir -p ~/.ssh +chmod 700 ~/.ssh +``` + +- [ ] **Step 2:编辑 `~/.ssh/config`** + +人工执行: + +```bash +nano ~/.ssh/config +``` + +写入以下内容: + +```sshconfig +Host wdd-pink-station + HostName 192.168.1.194 + Port 22 + User wdd + IdentityFile /mnt/c/Users/wddsh/.ssh/id_ed25519 + IdentitiesOnly yes + ServerAliveInterval 60 + ServerAliveCountMax 3 + StrictHostKeyChecking accept-new +``` + +保存后人工执行: + +```bash +chmod 600 ~/.ssh/config +``` + +- [ ] **Step 3:验证 Host 别名** + +人工执行: + +```bash +test -f ~/.ssh/config && echo WSL_SSH_CONFIG_OK +ssh wdd-pink-station "echo Connected" +ssh wdd-pink-station "wsl rsync --version" +ssh wdd-pink-station "wsl test -d /mnt/c/Users/wdd/xwechat_files && echo REMOTE_WSL_RSYNC_OK" +``` + +预期结果: + +```text +WSL_SSH_CONFIG_OK +Connected +REMOTE_WSL_RSYNC_OK +``` + +如果出现: + +```text +Can't open user config file ~/.ssh/config: No such file or directory +``` + +说明你当前是在笔记本 WSL2 中运行 rsync,但还没有在该 WSL 用户目录下创建 `~/.ssh/config`。回到本 Task 的 Step 1 和 Step 2,创建配置文件后再继续。Windows 侧的 `C:\Users\wddsh\.ssh\config` 不等于 WSL 侧的 `~/.ssh/config`。 + +--- + +## 3. Phase 2:微信数据 rsync 首次灌入 + +### Task 8:创建 dry-run 脚本 + +**操作位置:** 笔记本 WSL2。 + +- [ ] **Step 1:创建脚本目录** + +人工执行: + +```bash +mkdir -p ~/sync-scripts ~/wechat_backup/logs +``` + +- [ ] **Step 2:创建 dry-run 脚本** + +人工执行: + +```bash +nano ~/sync-scripts/wechat_initial_dry_run.sh +``` + +写入: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +SRC="/mnt/c/Users/wddsh/xwechat_files" +REMOTE_HOST="wdd-pink-station" +REMOTE_DST="/mnt/c/Users/wdd/xwechat_files" +REMOTE_RSYNC_PATH="wsl rsync" +SSH_CONFIG="$HOME/.ssh/config" +RSYNC_SSH="ssh -F ${SSH_CONFIG}" +LOG_DIR="/mnt/c/Users/wddsh/wechat_backup/logs" +LOG_FILE="${LOG_DIR}/wechat_initial_dry_run_$(date +%Y%m%d_%H%M%S).log" + +mkdir -p "$LOG_DIR" + +if tasklist.exe 2>/dev/null | grep -qi "Weixin.exe"; then + echo "ERROR: Weixin.exe is still running. Exit WeChat before dry-run." | tee -a "$LOG_FILE" + exit 2 +fi + +if [ ! -d "$SRC" ]; then + echo "ERROR: Source path does not exist: $SRC" | tee -a "$LOG_FILE" + exit 3 +fi + +if [ ! -f "$SSH_CONFIG" ]; then + echo "ERROR: Missing WSL SSH config: $SSH_CONFIG" | tee -a "$LOG_FILE" + echo "Run Task 7 first. Windows C:\\Users\\wddsh\\.ssh\\config is not the same file." | tee -a "$LOG_FILE" + exit 4 +fi + +$RSYNC_SSH "$REMOTE_HOST" "wsl test -d $REMOTE_DST" +$RSYNC_SSH "$REMOTE_HOST" "wsl rsync --version" >/dev/null + +rsync -rvzn \ + --itemize-changes \ + --partial \ + --inplace \ + --whole-file \ + --delete \ + --size-only \ + --no-owner \ + --no-group \ + --no-perms \ + --no-times \ + --omit-dir-times \ + --rsync-path="$REMOTE_RSYNC_PATH" \ + --exclude="temp/" \ + --exclude="cache/" \ + --exclude="apm_record/" \ + --exclude="crash_report/" \ + --exclude="log/" \ + --exclude="*.lock" \ + --exclude="*.tmp" \ + -e "$RSYNC_SSH" \ + "${SRC}/" \ + "${REMOTE_HOST}:${REMOTE_DST}/" \ + 2>&1 | tee -a "$LOG_FILE" + +echo "Dry-run log: $LOG_FILE" +``` + +- [ ] **Step 3:赋予执行权限** + +人工执行: + +```bash +chmod +x ~/sync-scripts/wechat_initial_dry_run.sh +``` + +- [ ] **Step 4:检查脚本没有混入 Markdown 代码围栏** + +人工执行: + +```bash +tail -n 5 ~/sync-scripts/wechat_initial_dry_run.sh +bash -n ~/sync-scripts/wechat_initial_dry_run.sh +``` + +预期结果: + +- `tail` 的最后一行应该是 `echo "Dry-run log: $LOG_FILE"`。 +- `tail` 输出中不应该出现单独一行由三个反引号组成的 Markdown 代码围栏。 +- `bash -n` 没有任何输出,并返回成功。 + +如果 `tail` 看到单独一行 ```` ``` ````,说明复制 Markdown 文档时把代码块结束标记也复制进了脚本。删除该行: + +```bash +sed -i '/^```$/d' ~/sync-scripts/wechat_initial_dry_run.sh +bash -n ~/sync-scripts/wechat_initial_dry_run.sh +``` + +`bash -n` 通过后再继续 Task 9。 + +### Task 9:执行 rsync dry-run 并审阅 + +**操作位置:** 笔记本 WSL2。 + +- [ ] **Step 1:运行 dry-run** + +人工执行: + +```bash +~/sync-scripts/wechat_initial_dry_run.sh +``` + +预期结果: + +- 输出大量 `>f+++++++++` 或目录创建记录,表示将复制到台式机。 +- 没有实际传输文件,因为参数包含 `-n`。 +- 没有 `deleting` 大量删除台式机有效数据;如果有,先停止并检查目标目录是否选错。 + +如果 rsync 已输出类似下面的统计信息: + +```text +total size is 36,671,651,105 speedup is ... (DRY RUN) +``` + +随后又报: + +```text +unexpected EOF while looking for matching ``' +``` + +说明 dry-run 主体已经完成,但脚本文件里混入了未配对的反引号,通常是末尾复制了 Markdown 代码围栏 ```` ``` ````。处理: + +```bash +tail -n 10 ~/sync-scripts/wechat_initial_dry_run.sh +sed -i '/^```$/d' ~/sync-scripts/wechat_initial_dry_run.sh +bash -n ~/sync-scripts/wechat_initial_dry_run.sh +``` + +`bash -n` 没有输出后,再重新执行 dry-run。重新执行的目的只是确认脚本退出码正常;前一次 dry-run 已经证明 rsync 连接链路可用。 + +- [ ] **Step 2:检查 dry-run 日志** + +人工执行: + +```bash +ls -lh /mnt/c/Users/wddsh/wechat_backup/logs/ +``` + +再打开最新的 `wechat_initial_dry_run_*.log`,重点检查: + +- 源路径是 `/mnt/c/Users/wddsh/xwechat_files/`。 +- 目标路径是 `wdd-pink-station:/mnt/c/Users/wdd/xwechat_files/`。 +- 脚本包含 `--rsync-path="wsl rsync"`,表示远端 rsync 由台式机 WSL2 提供。 +- 脚本使用 `-rvzn`,不要使用 `-a`。`-a` 会隐式启用权限、属主、属组、时间戳等 Linux 元数据保留行为,不适合远端目标为 Windows `/mnt/c` 的微信目录。 +- 脚本包含 `--size-only`、`--no-owner`、`--no-group`、`--no-perms`、`--no-times`、`--omit-dir-times`,表示用文件大小判断增量,不在 Windows 盘上保留 Linux owner/group/permission/time 元数据。 +- 脚本包含 `--inplace`、`--whole-file`,表示直接写目标文件并按整文件复制,减少 rsync 临时文件在 Windows 盘上的元数据操作。 +- 没有把目标写到 `/mnt/c/Users/wdd/wechat_files/xwechat_files/`。 + +- [ ] **Step 3:如果 dry-run 报 SSH config 缺失** + +如果输出: + +```text +Can't open user config file ~/.ssh/config: No such file or directory +``` + +原因是当前脚本运行在笔记本 WSL2,`-e "ssh -F ~/.ssh/config"` 查找的是 WSL 用户目录下的: + +```text +~/.ssh/config +``` + +不是 Windows 目录: + +```text +C:\Users\wddsh\.ssh\config +``` + +处理步骤: + +```bash +mkdir -p ~/.ssh +nano ~/.ssh/config +chmod 600 ~/.ssh/config +test -f ~/.ssh/config && echo WSL_SSH_CONFIG_OK +``` + +`~/.ssh/config` 内容必须与 Task 7 完全一致。完成后先验证: + +```bash +ssh wdd-pink-station "echo Connected" +ssh wdd-pink-station "wsl rsync --version" +``` + +再重新运行: + +```bash +~/sync-scripts/wechat_initial_dry_run.sh +``` + +如果以下手动验证已经成功: + +```bash +test -f ~/.ssh/config && echo WSL_SSH_CONFIG_OK +ssh wdd-pink-station "echo Connected" +ssh wdd-pink-station "wsl rsync --version" +ssh wdd-pink-station "wsl test -d /mnt/c/Users/wdd/xwechat_files && echo REMOTE_WSL_RSYNC_OK" +``` + +但 dry-run 仍然报: + +```text +Can't open user config file ~/.ssh/config: No such file or directory +``` + +说明正在运行的 `~/sync-scripts/wechat_initial_dry_run.sh` 仍是旧版本,脚本中还残留字面量 `~/.ssh/config`。检查: + +```bash +grep -n "ssh -F" ~/sync-scripts/wechat_initial_dry_run.sh +grep -n "RSYNC_SSH" ~/sync-scripts/wechat_initial_dry_run.sh +``` + +正确脚本必须包含: + +```bash +SSH_CONFIG="$HOME/.ssh/config" +RSYNC_SSH="ssh -F ${SSH_CONFIG}" +``` + +并且 rsync 命令中必须是: + +```bash +-e "$RSYNC_SSH" \ +``` + +不能再出现: + +```bash +-e "ssh -F ~/.ssh/config" \ +``` + +- [ ] **Step 4:如果 dry-run 报远端 rsync 或 wsl 不存在** + +如果输出包含: + +```text +wsl: command not found +rsync: command not found +rsync error: unexplained error (code 255) +``` + +说明台式机侧还没有完成 WSL2 或 rsync 准备。处理步骤: + +1. 回到 Task 6a,在台式机安装 WSL2 和 rsync。 +2. 在笔记本 WSL2 中执行: + +```bash +ssh wdd-pink-station "wsl rsync --version" +ssh wdd-pink-station "wsl test -d /mnt/c/Users/wdd/xwechat_files && echo REMOTE_RSYNC_READY" +``` + +3. 看到 `REMOTE_RSYNC_READY` 后,再重新运行 dry-run。 + +### Task 10:创建首次正式灌入脚本 + +**操作位置:** 笔记本 WSL2。 + +- [ ] **Step 1:创建正式脚本** + +人工执行: + +```bash +nano ~/sync-scripts/wechat_initial_sync.sh +``` + +写入: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +SRC="/mnt/c/Users/wddsh/xwechat_files" +REMOTE_HOST="wdd-pink-station" +REMOTE_DST="/mnt/c/Users/wdd/xwechat_files" +REMOTE_RSYNC_PATH="wsl rsync" +SSH_CONFIG="$HOME/.ssh/config" +RSYNC_SSH="ssh -F ${SSH_CONFIG}" +LOG_DIR="/mnt/c/Users/wddsh/wechat_backup/logs" +LOG_FILE="${LOG_DIR}/wechat_initial_sync_$(date +%Y%m%d_%H%M%S).log" + +mkdir -p "$LOG_DIR" + +echo "======== WeChat initial rsync started at $(date '+%Y-%m-%d %H:%M:%S') ========" | tee -a "$LOG_FILE" + +if tasklist.exe 2>/dev/null | grep -qi "Weixin.exe"; then + echo "ERROR: Weixin.exe is still running. Exit WeChat before initial sync." | tee -a "$LOG_FILE" + exit 2 +fi + +if [ ! -d "$SRC" ]; then + echo "ERROR: Source path does not exist: $SRC" | tee -a "$LOG_FILE" + exit 3 +fi + +if [ ! -f "$SSH_CONFIG" ]; then + echo "ERROR: Missing WSL SSH config: $SSH_CONFIG" | tee -a "$LOG_FILE" + echo "Run Task 7 first. Windows C:\\Users\\wddsh\\.ssh\\config is not the same file." | tee -a "$LOG_FILE" + exit 4 +fi + +$RSYNC_SSH "$REMOTE_HOST" "wsl mkdir -p $REMOTE_DST" +$RSYNC_SSH "$REMOTE_HOST" "wsl rsync --version" >/dev/null + +rsync -rvz \ + --info=progress2 \ + --partial \ + --inplace \ + --whole-file \ + --delete \ + --size-only \ + --no-owner \ + --no-group \ + --no-perms \ + --no-times \ + --omit-dir-times \ + --rsync-path="$REMOTE_RSYNC_PATH" \ + --exclude="temp/" \ + --exclude="cache/" \ + --exclude="apm_record/" \ + --exclude="crash_report/" \ + --exclude="log/" \ + --exclude="*.lock" \ + --exclude="*.tmp" \ + -e "$RSYNC_SSH" \ + "${SRC}/" \ + "${REMOTE_HOST}:${REMOTE_DST}/" \ + 2>&1 | tee -a "$LOG_FILE" + +status=${PIPESTATUS[0]} +echo "======== WeChat initial rsync finished with status ${status} at $(date '+%Y-%m-%d %H:%M:%S') ========" | tee -a "$LOG_FILE" +exit "$status" +``` + +- [ ] **Step 2:赋予执行权限** + +人工执行: + +```bash +chmod +x ~/sync-scripts/wechat_initial_sync.sh +``` + +- [ ] **Step 3:检查正式脚本没有混入 Markdown 代码围栏** + +人工执行: + +```bash +tail -n 5 ~/sync-scripts/wechat_initial_sync.sh +bash -n ~/sync-scripts/wechat_initial_sync.sh +``` + +预期结果: + +- `tail` 的最后一行应该是 `exit "$status"`。 +- `tail` 输出中不应该出现单独一行 ```` ``` ````。 +- `bash -n` 没有任何输出,并返回成功。 + +如果发现代码围栏,删除后复查: + +```bash +sed -i '/^```$/d' ~/sync-scripts/wechat_initial_sync.sh +bash -n ~/sync-scripts/wechat_initial_sync.sh +``` + +### Task 11:执行首次正式灌入 + +**操作位置:** 笔记本 WSL2。 + +- [ ] **Step 1:再次确认微信未运行** + +人工执行: + +```bash +tasklist.exe 2>/dev/null | grep -i "Weixin.exe" || echo "WeChat is not running" +``` + +预期结果: + +```text +WeChat is not running +``` + +- [ ] **Step 2:运行正式灌入** + +人工执行: + +```bash +~/sync-scripts/wechat_initial_sync.sh +``` + +预期结果: + +- 首次可能运行 1 到 2 小时,取决于文件数量、磁盘和局域网速度。 +- 退出状态为 `0`。 +- 日志保存在 `C:\Users\wddsh\wechat_backup\logs`。 + +如果输出大量类似下面的警告: + +```text +rsync: [generator] chgrp "/mnt/c/Users/wdd/xwechat_files/..." failed: Operation not permitted (1) +``` + +根因是 `rsync -a` 默认尝试保留 Linux group 元数据,但目标目录位于台式机 Windows 盘 `/mnt/c`,Windows 文件系统不接受 WSL 的 `chgrp`。处理方式: + +1. 不需要手动修改这些目标文件。 +2. 暂停当前脚本或等待其结束都可以;如果已经传了很多文件,重新运行修正后的脚本会增量续传。 +3. 确认 `~/sync-scripts/wechat_initial_sync.sh` 的 rsync 参数中已经包含: + +```bash +--size-only \ +--no-owner \ +--no-group \ +--no-perms \ +--no-times \ +--omit-dir-times \ +``` + +4. 语法检查: + +```bash +bash -n ~/sync-scripts/wechat_initial_sync.sh +``` + +5. 重新执行: + +```bash +~/sync-scripts/wechat_initial_sync.sh +``` + +预期结果:不再出现 `chgrp ... Operation not permitted`。如果仍有少量 `Permission denied`,优先检查微信是否正在运行,或者目标文件是否被台式机微信、杀毒软件、索引服务占用。 + +如果输出类似下面的警告: + +```text +rsync: [generator] failed to set times on "/mnt/c/Users/wdd/xwechat_files/.": Operation not permitted (1) +``` + +根因是 `rsync -a` 默认保留时间戳,其中目录时间戳在 Windows `/mnt/c` 上可能无法设置。处理方式: + +1. 确认 `~/sync-scripts/wechat_initial_sync.sh` 的 rsync 参数中已经包含: + +```bash +--omit-dir-times \ +``` + +2. 同时保留前面的 Windows 元数据规避参数: + +```bash +--size-only \ +--no-owner \ +--no-group \ +--no-perms \ +--no-times \ +--omit-dir-times \ +``` + +3. 语法检查并重新执行: + +```bash +bash -n ~/sync-scripts/wechat_initial_sync.sh +~/sync-scripts/wechat_initial_sync.sh +``` + +预期结果:不再出现 `failed to set times on ".../."`。`--omit-dir-times` 只跳过目录时间戳;如果仍然出现临时文件或普通文件的 `failed to set times`,说明还在使用 `-a` 或仍在保留文件时间戳,继续执行下面的 code 23 完整收敛方案。 + +如果脚本已经传输到 100%,最后输出: + +```text +rsync error: some files/attrs were not transferred (see previous errors) (code 23) +``` + +并且前面的错误主要是: + +```text +rsync: [receiver] failed to set times on "/mnt/c/Users/wdd/xwechat_files/...": Operation not permitted (1) +``` + +这是 Windows `/mnt/c` 文件时间戳元数据写入失败。内容已经基本传完,但 rsync 因属性设置失败返回 `23`。完整解决步骤: + +1. 不删除台式机 `C:\Users\wdd\xwechat_files`,保留已经传过去的 36GB 数据。 +2. 把 `~/sync-scripts/wechat_initial_sync.sh` 中的 rsync 起始行从 `rsync -avz \` 改为: + +```bash +rsync -rvz \ +``` + +3. 确认参数包含以下完整 Windows-safe 参数组: + +```bash +--partial \ +--inplace \ +--whole-file \ +--delete \ +--size-only \ +--no-owner \ +--no-group \ +--no-perms \ +--no-times \ +--omit-dir-times \ +``` + +4. 确认脚本中不再出现 `rsync -avz`: + +```bash +grep -n "rsync -" ~/sync-scripts/wechat_initial_sync.sh +grep -n -- "--no-times\|--size-only\|--inplace" ~/sync-scripts/wechat_initial_sync.sh +bash -n ~/sync-scripts/wechat_initial_sync.sh +``` + +预期结果: + +- 第一条显示 `rsync -rvz \`。 +- 第二条能看到 `--size-only`、`--inplace`、`--no-times`。 +- `bash -n` 没有输出。 + +5. 重新执行一次收敛同步: + +```bash +~/sync-scripts/wechat_initial_sync.sh +``` + +预期结果: + +- 因为已经传过 36GB,第二次主要做增量检查和少量补齐。 +- 不再出现 `failed to set times`。 +- 最终退出状态为 `0`。 + +6. 如果仍然出现 `failed to set times`,说明当前运行的还是旧脚本。直接检查: + +```bash +nl -ba ~/sync-scripts/wechat_initial_sync.sh | sed -n '1,90p' +``` + +确认实际运行脚本与文档中的 Task 10 完全一致。 + +- [ ] **Step 3:验证台式机文件规模** + +在台式机 PowerShell 人工执行: + +```powershell +(Get-ChildItem "C:\Users\wdd\xwechat_files" -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object).Count +``` + +在笔记本 PowerShell 人工执行: + +```powershell +(Get-ChildItem "C:\Users\wddsh\xwechat_files" -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object).Count +``` + +预期结果:两个数量接近。排除目录导致少量差异是正常的。 + +--- + +## 4. Phase 3:安装并配对 Syncthing + +### Task 12:下载并安装 Syncthing + +**操作位置:** 笔记本和台式机都执行。 + +- [ ] **Step 1:打开官方下载页** + +在浏览器访问: + +```text +https://syncthing.net/downloads/ +``` + +选择 Windows 版本。Windows 当前没有官方安装器,官方文档给出的常规方式是下载压缩包并手动放置可执行文件。 + +- [ ] **Step 2:创建安装目录** + +以普通用户或管理员 PowerShell 人工执行: + +```powershell +New-Item -ItemType Directory -Force -Path "C:\Tools\Syncthing" +``` + +- [ ] **Step 3:解压** + +把下载的 Syncthing Windows 压缩包解压,把其中的 `syncthing.exe` 放到: + +```text +C:\Tools\Syncthing\syncthing.exe +``` + +- [ ] **Step 4:首次交互启动** + +双击: + +```text +C:\Tools\Syncthing\syncthing.exe +``` + +Windows 防火墙弹窗出现时,勾选专用网络,允许访问。 + +- [ ] **Step 5:打开 Web UI** + +访问: + +```text +http://127.0.0.1:8384 +``` + +预期结果:打开 Syncthing 管理界面。 + +### Task 13:加固 Syncthing Web UI + +**操作位置:** 笔记本和台式机都执行。 + +- [ ] **Step 1:进入 GUI 设置** + +在 Syncthing Web UI 中点击: + +```text +Actions -> Settings -> GUI +``` + +- [ ] **Step 2:设置用户名和密码** + +填写: + +```text +GUI Authentication User: wddsh +GUI Authentication Password: 使用本机密码管理器生成的强密码 +``` + +台式机用户名可以填: + +```text +wdd +``` + +- [ ] **Step 3:限制监听地址** + +确认 GUI Listen Address 保持: + +```text +127.0.0.1:8384 +``` + +不要改成 `0.0.0.0:8384`,除非后续明确需要远程访问 Web UI。 + +- [ ] **Step 4:保存并重启 Syncthing** + +点击保存。提示重启时,点击重启。 + +### Task 14:配对两台设备 + +**操作位置:** 两端 Syncthing Web UI。 + +- [ ] **Step 1:在笔记本复制 Device ID** + +打开笔记本 Syncthing: + +```text +Actions -> Show ID +``` + +复制完整 Device ID。 + +- [ ] **Step 2:在台式机添加笔记本** + +打开台式机 Syncthing: + +```text +Add Remote Device +``` + +填写: + +```text +Device ID: 粘贴笔记本 Device ID +Device Name: R9000P-wddsh +``` + +保存。 + +- [ ] **Step 3:在台式机复制 Device ID** + +打开台式机 Syncthing: + +```text +Actions -> Show ID +``` + +复制完整 Device ID。 + +- [ ] **Step 4:在笔记本添加台式机** + +打开笔记本 Syncthing: + +```text +Add Remote Device +``` + +填写: + +```text +Device ID: 粘贴台式机 Device ID +Device Name: Desktop-wdd +``` + +保存。 + +- [ ] **Step 5:确认直连** + +在两端 Web UI 的 Remote Devices 中确认连接状态。 + +预期结果: + +```text +Connected +Connection Type: TCP LAN 或 QUIC LAN +``` + +如果显示 Relay,按以下顺序处理: + +1. 确认 Windows 网络配置为专用网络。 +2. 确认防火墙允许 `C:\Tools\Syncthing\syncthing.exe`。 +3. 确认两机在同一局域网。 +4. 重新启动 Syncthing。 + +--- + +## 5. Phase 4:微信目录 Syncthing 持续同步 + +### Task 15:添加微信共享文件夹 + +**操作位置:** 先笔记本,后台式机。 + +- [ ] **Step 1:在笔记本添加文件夹** + +笔记本 Syncthing Web UI 点击: + +```text +Add Folder +``` + +填写: + +```text +Folder Label: WeChat xwechat_files +Folder ID: wechat-xwechat-files +Folder Path: C:\Users\wddsh\xwechat_files +Folder Type: Send & Receive +``` + +- [ ] **Step 2:共享给台式机** + +在 Sharing 页签勾选: + +```text +Desktop-wdd +``` + +- [ ] **Step 3:设置扫描与监控** + +在 Advanced 页签设置: + +```text +Watch for Changes: Enabled +Full Rescan Interval: 300 seconds +Ignore Permissions: Enabled +``` + +- [ ] **Step 4:启用版本控制** + +在 File Versioning 页签选择: + +```text +Staggered File Versioning +``` + +设置: + +```text +Maximum Age: 30 days +``` + +- [ ] **Step 5:保存** + +点击 Save。 + +- [ ] **Step 6:在台式机接受共享** + +台式机 Web UI 会弹出新文件夹邀请,点击 Add。 + +填写: + +```text +Folder Path: C:\Users\wdd\xwechat_files +Folder Type: Send & Receive +Watch for Changes: Enabled +Full Rescan Interval: 300 seconds +Ignore Permissions: Enabled +File Versioning: Staggered, 30 days +``` + +保存。 + +### Task 16:配置微信 `.stignore` + +**操作位置:** 笔记本和台式机的微信同步目录都配置。 + +- [ ] **Step 1:在笔记本创建忽略文件** + +用记事本打开或创建: + +```text +C:\Users\wddsh\xwechat_files\.stignore +``` + +写入: + +```gitignore +// Windows noise +(?i)desktop.ini +(?i)thumbs.db + +// WeChat volatile files +temp/** +cache/** +apm_record/** +crash_report/** +log/** +*.lock +*.tmp +*.temp +*.bak.tmp + +// Syncthing conflict review output should be handled manually +*.sync-conflict-* +``` + +- [ ] **Step 2:在台式机创建同样的忽略文件** + +用记事本打开或创建: + +```text +C:\Users\wdd\xwechat_files\.stignore +``` + +写入同样内容。 + +> [!WARNING] +> **db_storage 动态保护:** 高级解决方案要求微信运行时临时将 `db_storage` 排除同步(SQLite 数据库锁定状态下同步可能导致损坏)。`.stignore` 中**不要**永久添加 `db_storage`,否则聊天记录数据库将永远不会同步。下方 Task 18a 提供了自动保护脚本,可在微信运行时动态排除、关闭后自动恢复。 + +- [ ] **Step 3:触发重新扫描** + +在两端 Syncthing Web UI 中,对 `WeChat xwechat_files` 点击: + +```text +Rescan +``` + +### Task 17:微信同步验收 + +**操作位置:** 两台机器。 + +- [ ] **Step 1:等待状态变为 Up to Date** + +在两端 Web UI 确认 `WeChat xwechat_files` 状态: + +```text +Up to Date +``` + +- [ ] **Step 2:笔记本创建测试文件** + +在笔记本 PowerShell 人工执行: + +```powershell +Set-Content -Path "C:\Users\wddsh\xwechat_files\syncthing_probe_from_laptop.txt" -Value "from laptop $(Get-Date -Format s)" +``` + +- [ ] **Step 3:台式机确认收到** + +在台式机 PowerShell 人工执行: + +```powershell +Get-Content "C:\Users\wdd\xwechat_files\syncthing_probe_from_laptop.txt" +``` + +预期结果:能看到 `from laptop`。 + +- [ ] **Step 4:台式机创建反向测试文件** + +在台式机 PowerShell 人工执行: + +```powershell +Set-Content -Path "C:\Users\wdd\xwechat_files\syncthing_probe_from_desktop.txt" -Value "from desktop $(Get-Date -Format s)" +``` + +- [ ] **Step 5:笔记本确认收到** + +在笔记本 PowerShell 人工执行: + +```powershell +Get-Content "C:\Users\wddsh\xwechat_files\syncthing_probe_from_desktop.txt" +``` + +预期结果:能看到 `from desktop`。 + +- [ ] **Step 6:删除测试文件** + +在笔记本 PowerShell 人工执行: + +```powershell +Remove-Item "C:\Users\wddsh\xwechat_files\syncthing_probe_from_laptop.txt" -Force +Remove-Item "C:\Users\wddsh\xwechat_files\syncthing_probe_from_desktop.txt" -Force +``` + +等待删除同步到台式机。 + +### Task 18:微信日常使用规则 + +- [ ] **Step 1:切换电脑前退出微信** + +从笔记本切到台式机,或从台式机切到笔记本前,先退出微信。 + +- [ ] **Step 2:等待 Syncthing 同步完成** + +打开 `http://127.0.0.1:8384`,确认微信文件夹是: + +```text +Up to Date +``` + +- [ ] **Step 3:再在另一台电脑登录微信** + +不要在 Syncthing 显示 `Syncing`、`Scanning`、`Out of Sync` 时切换微信主力设备。 + +### Task 18a:微信 db_storage 动态保护(建议配置) + +> [!IMPORTANT] +> 高级解决方案要求:微信运行时临时排除 `db_storage` 目录(SQLite 数据库锁定状态下同步可能导致文件损坏),微信关闭后恢复同步。以下脚本自动完成这一切换,建议配合任务计划程序每 2 分钟执行一次。 + +**操作位置:** 笔记本和台式机都配置。 + +- [ ] **Step 1:获取 Syncthing API Key** + +在 Syncthing Web UI 中复制: + +```text +Actions -> Settings -> General -> API Key +``` + +然后在本机 PowerShell 中保存到当前用户目录下的受限文件: + +```powershell +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\syncthing-scripts" +$apiKey = Read-Host "Paste this machine's Syncthing API Key" +Set-Content -Path "$env:USERPROFILE\syncthing-scripts\syncthing-api-key.txt" -Value $apiKey -NoNewline +icacls "$env:USERPROFILE\syncthing-scripts\syncthing-api-key.txt" /inheritance:r +icacls "$env:USERPROFILE\syncthing-scripts\syncthing-api-key.txt" /grant:r "$env:USERNAME:F" +``` + +预期结果:`syncthing-api-key.txt` 只允许当前 Windows 用户读取。 + +- [ ] **Step 2:创建保护脚本** + +人工执行创建脚本目录: + +```powershell +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\syncthing-scripts" +``` + +用记事本创建脚本文件: + +笔记本:`C:\Users\wddsh\syncthing-scripts\wechat-db-guard.ps1` +台式机:`C:\Users\wdd\syncthing-scripts\wechat-db-guard.ps1` + +写入以下内容(两端相同): + +```powershell +param( + [Parameter(Mandatory)][string]$StIgnorePath, + [Parameter(Mandatory)][string]$ApiKeyPath +) +$ErrorActionPreference = "Stop" +$guardComment = "// [AUTO-GUARD] db_storage excluded while WeChat is running" +$guardLine = "**/db_storage/**" + +$wechatRunning = [bool](Get-Process Weixin -ErrorAction SilentlyContinue) + +if (-not (Test-Path $StIgnorePath)) { + Write-Host "ERROR: .stignore not found: $StIgnorePath" + exit 1 +} +if (-not (Test-Path $ApiKeyPath)) { + Write-Host "ERROR: Syncthing API key file not found: $ApiKeyPath" + exit 2 +} + +$ApiKey = (Get-Content $ApiKeyPath -Raw).Trim() +$content = Get-Content $StIgnorePath -Raw +$hasGuard = $content.Contains($guardLine) + +if ($wechatRunning -and -not $hasGuard) { + Add-Content -Path $StIgnorePath -Value "`n$guardComment`n$guardLine" + Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:8384/rest/db/scan?folder=wechat-xwechat-files" -Headers @{"X-API-Key"=$ApiKey} | Out-Null + Write-Host "[$(Get-Date -Format s)] GUARD ON: db_storage excluded (WeChat running)" +} +elseif (-not $wechatRunning -and $hasGuard) { + (Get-Content $StIgnorePath) | Where-Object { $_ -ne $guardLine -and $_ -ne $guardComment } | Set-Content $StIgnorePath + Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:8384/rest/db/scan?folder=wechat-xwechat-files" -Headers @{"X-API-Key"=$ApiKey} | Out-Null + Write-Host "[$(Get-Date -Format s)] GUARD OFF: db_storage syncing (WeChat closed)" +} +else { + Write-Host "[$(Get-Date -Format s)] No action needed" +} +``` + +- [ ] **Step 3:手动验证脚本** + +微信**运行时**在 PowerShell 执行(笔记本示例): + +```powershell +powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\syncthing-scripts\wechat-db-guard.ps1" -StIgnorePath "C:\Users\wddsh\xwechat_files\.stignore" -ApiKeyPath "$env:USERPROFILE\syncthing-scripts\syncthing-api-key.txt" +``` + +台式机将 `-StIgnorePath` 替换为 `"C:\Users\wdd\xwechat_files\.stignore"`。 + +预期结果:输出 `GUARD ON`,检查 `.stignore` 末尾出现 `**/db_storage/**`。 + +关闭微信后再次执行,预期输出 `GUARD OFF`,guard 行被移除。 + +- [ ] **Step 4:配置定时执行** + +打开任务计划程序(`taskschd.msc`),创建新任务: + +```text +Name: WeChat DB Guard +Triggers: At log on, Repeat every 2 minutes indefinitely +Action: Start a program +Program/script: powershell.exe +``` + +笔记本 Add arguments: + +```text +-ExecutionPolicy Bypass -WindowStyle Hidden -File "C:\Users\wddsh\syncthing-scripts\wechat-db-guard.ps1" -StIgnorePath "C:\Users\wddsh\xwechat_files\.stignore" -ApiKeyPath "C:\Users\wddsh\syncthing-scripts\syncthing-api-key.txt" +``` + +台式机 Add arguments: + +```text +-ExecutionPolicy Bypass -WindowStyle Hidden -File "C:\Users\wdd\syncthing-scripts\wechat-db-guard.ps1" -StIgnorePath "C:\Users\wdd\xwechat_files\.stignore" -ApiKeyPath "C:\Users\wdd\syncthing-scripts\syncthing-api-key.txt" +``` + +Settings 页: + +```text +勾选 Allow task to be run on demand +取消勾选 Stop the task if it runs longer than +If the task is already running: Do not start a new instance +``` + +--- + +## 6. Phase 5:Git 项目仓库 Syncthing 同步 + +### Task 19:实施前清理 Git 工作状态 + +**操作位置:** 笔记本和台式机。 + +- [ ] **Step 1:关闭 IDE** + +关闭所有可能写入项目目录的 IDE、编辑器、构建工具、终端。 + +- [ ] **Step 2:检查常用仓库状态** + +在每个活跃仓库执行: + +```powershell +git status +``` + +预期结果: + +- 没有 rebase、merge、cherry-pick 进行中。 +- 如果有重要未提交改动,先在原机器上 commit 或 stash。 + +- [ ] **Step 3:删除临时锁文件** + +只在确认没有 Git 命令运行时,人工检查并删除: + +```powershell +Get-ChildItem "C:\Users\wddsh\Documents\IdeaProjects" -Recurse -Force -Filter "index.lock" +``` + +如果确认是残留锁文件,删除: + +```powershell +Get-ChildItem "C:\Users\wddsh\Documents\IdeaProjects" -Recurse -Force -Filter "index.lock" | Remove-Item -Force +``` + +台式机同样检查: + +```powershell +Get-ChildItem "C:\Users\wdd\Documents\IdeaProjects" -Recurse -Force -Filter "index.lock" +``` + +### Task 20:添加 Git 项目共享文件夹 + +**操作位置:** 先笔记本,后台式机。 + +- [ ] **Step 1:在笔记本添加文件夹** + +Syncthing Web UI 点击: + +```text +Add Folder +``` + +填写: + +```text +Folder Label: IdeaProjects +Folder ID: ideaprojects +Folder Path: C:\Users\wddsh\Documents\IdeaProjects +Folder Type: Send & Receive +``` + +- [ ] **Step 2:共享给台式机** + +Sharing 页签勾选: + +```text +Desktop-wdd +``` + +- [ ] **Step 3:设置高级参数** + +Advanced 页签设置: + +```text +Watch for Changes: Enabled +Full Rescan Interval: 600 seconds +Ignore Permissions: Enabled +``` + +- [ ] **Step 4:启用版本控制** + +File Versioning 页签选择: + +```text +Staggered File Versioning +Maximum Age: 30 days +``` + +- [ ] **Step 5:台式机接受共享** + +台式机 Web UI 收到邀请后点击 Add,填写: + +```text +Folder Path: C:\Users\wdd\Documents\IdeaProjects +Folder Type: Send & Receive +Watch for Changes: Enabled +Full Rescan Interval: 600 seconds +Ignore Permissions: Enabled +File Versioning: Staggered, 30 days +``` + +保存。 + +### Task 21:配置 Git 项目 `.stignore` + +**操作位置:** 笔记本和台式机的 `IdeaProjects` 根目录都配置。 + +- [ ] **Step 1:在笔记本创建忽略文件** + +创建或编辑: + +```text +C:\Users\wddsh\Documents\IdeaProjects\.stignore +``` + +写入: + +```gitignore +// Windows noise +(?i)desktop.ini +(?i)thumbs.db + +// Dependency directories +**/node_modules/** +**/.pnpm-store/** +**/.yarn/cache/** +**/.gradle/** +**/.m2/repository/** +**/vendor/** + +// Build outputs +**/build/** +**/dist/** +**/out/** +**/target/** +**/.next/** +**/.nuxt/** +**/.vite/** +**/coverage/** + +// Python caches +**/__pycache__/** +**/*.pyc +**/.pytest_cache/** +**/.mypy_cache/** +**/.ruff_cache/** + +// IDE volatile files +**/.idea/workspace.xml +**/.idea/tasks.xml +**/.idea/usage.statistics.xml +**/.idea/shelf/** +**/*.iws +**/.vscode/.ropeproject/** + +// Logs and temp +**/*.log +**/*.tmp +**/*.temp +**/.DS_Store + +// Git transient lock files; do not ignore .git itself +**/.git/index.lock +**/.git/HEAD.lock +**/.git/config.lock + +// Syncthing conflicts require manual resolution +**/*.sync-conflict-* +``` + +- [ ] **Step 2:在台式机创建同样的忽略文件** + +创建或编辑: + +```text +C:\Users\wdd\Documents\IdeaProjects\.stignore +``` + +写入同样内容。 + +- [ ] **Step 3:触发重新扫描** + +在两端 Web UI 对 `IdeaProjects` 点击: + +```text +Rescan +``` + +### Task 22:Git 项目同步验收 + +**操作位置:** 两台机器。 + +- [ ] **Step 1:等待状态为 Up to Date** + +在两端确认 `IdeaProjects` 文件夹状态: + +```text +Up to Date +``` + +- [ ] **Step 2:笔记本创建测试文件** + +在笔记本 PowerShell 人工执行: + +```powershell +Set-Content -Path "C:\Users\wddsh\Documents\IdeaProjects\syncthing_project_probe.txt" -Value "from laptop $(Get-Date -Format s)" +``` + +- [ ] **Step 3:台式机确认收到** + +在台式机 PowerShell 人工执行: + +```powershell +Get-Content "C:\Users\wdd\Documents\IdeaProjects\syncthing_project_probe.txt" +``` + +预期结果:能看到 `from laptop`。 + +- [ ] **Step 4:台式机创建反向测试文件** + +在台式机 PowerShell 人工执行: + +```powershell +Set-Content -Path "C:\Users\wdd\Documents\IdeaProjects\syncthing_project_probe_desktop.txt" -Value "from desktop $(Get-Date -Format s)" +``` + +- [ ] **Step 5:笔记本确认收到** + +在笔记本 PowerShell 人工执行: + +```powershell +Get-Content "C:\Users\wddsh\Documents\IdeaProjects\syncthing_project_probe_desktop.txt" +``` + +- [ ] **Step 6:删除测试文件** + +在笔记本 PowerShell 人工执行: + +```powershell +Remove-Item "C:\Users\wddsh\Documents\IdeaProjects\syncthing_project_probe.txt" -Force +Remove-Item "C:\Users\wddsh\Documents\IdeaProjects\syncthing_project_probe_desktop.txt" -Force +``` + +### Task 23:Git 日常使用规则 + +- [ ] **Step 1:同一时间只在一台机器开发同一个仓库** + +如果笔记本正在编辑某个仓库,台式机只做查看或等待同步完成。 + +- [ ] **Step 2:大改动先 commit** + +开始跨机器切换前,在当前机器执行: + +```powershell +git status +git add -A +git commit -m "wip: save work before machine switch" +``` + +如果不想产生 commit,使用: + +```powershell +git stash push -u -m "machine switch" +``` + +- [ ] **Step 3:切换机器前等待 Up to Date** + +确认 `IdeaProjects` 在 Syncthing 中为: + +```text +Up to Date +``` + +- [ ] **Step 4:另一台机器先检查状态再开发** + +在目标机器仓库中执行: + +```powershell +git status +``` + +确认没有异常冲突文件后再打开 IDE。 + +--- + +## 7. Phase 6:开发工具配置同步 + +### Task 24:优先启用内置配置同步 + +**操作位置:** 两台机器。 + +- [ ] **Step 1:JetBrains IDE 启用 Settings Sync** + +在 IntelliJ IDEA、WebStorm 或其他 JetBrains IDE 中打开: + +```text +File -> Settings -> Settings Sync +``` + +登录同一个 JetBrains 账号,启用同步。 + +- [ ] **Step 2:VS Code 启用 Settings Sync** + +在 VS Code 中执行: + +```text +Manage -> Settings Sync is On +``` + +登录同一个 GitHub 或 Microsoft 账号。 + +- [ ] **Step 3:不要用 Syncthing 同步 JetBrains 整个配置目录** + +不要同步整个: + +```text +%APPDATA%\JetBrains +``` + +原因:其中包含缓存、索引、机器路径和可能的平台特定状态,跨机器直接同步容易导致 IDE 异常。 + +### Task 25:同步 Windows Terminal 配置 + +**操作位置:** 如果两端都使用 Microsoft Store 版 Windows Terminal。 + +- [ ] **Step 1:确认配置路径** + +笔记本路径: + +```text +C:\Users\wddsh\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState +``` + +台式机路径: + +```text +C:\Users\wdd\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState +``` + +- [ ] **Step 2:在 Syncthing 添加独立文件夹** + +笔记本添加: + +```text +Folder Label: Windows Terminal +Folder ID: windows-terminal-settings +Folder Path: C:\Users\wddsh\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState +Folder Type: Send & Receive +``` + +共享给 `Desktop-wdd`。 + +- [ ] **Step 3:台式机接受共享** + +填写: + +```text +Folder Path: C:\Users\wdd\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState +Folder Type: Send & Receive +``` + +- [ ] **Step 4:配置 `.stignore`** + +两端的 `LocalState\.stignore` 写入: + +```gitignore +*.tmp +*.log +*.bak +``` + +### Task 26:一次性迁移 Git 全局配置 + +**操作位置:** 笔记本导出,台式机导入。 + +- [ ] **Step 1:在笔记本查看 Git 全局配置** + +人工执行: + +```powershell +git config --global --list --show-origin +``` + +- [ ] **Step 2:在笔记本复制全局配置文件** + +全局配置文件通常为: + +```text +C:\Users\wddsh\.gitconfig +``` + +人工复制到台式机: + +```text +C:\Users\wdd\.gitconfig +``` + +- [ ] **Step 3:台式机审阅绝对路径** + +在台式机 PowerShell 执行: + +```powershell +notepad "C:\Users\wdd\.gitconfig" +``` + +检查并修正包含 `C:\Users\wddsh` 的路径。 + +### Task 27:一次性迁移 SSH 配置 + +**操作位置:** 手动操作,不建议 Syncthing 长期同步私钥。 + +- [ ] **Step 1:复制 SSH config,不复制私钥前先评估** + +可复制: + +```text +C:\Users\wddsh\.ssh\config +``` + +到: + +```text +C:\Users\wdd\.ssh\config +``` + +- [ ] **Step 2:私钥按需迁移** + +如果确实需要同一套私钥,在台式机创建: + +```powershell +New-Item -ItemType Directory -Force -Path "C:\Users\wdd\.ssh" +``` + +然后手动复制: + +```text +C:\Users\wddsh\.ssh\id_ed25519 +C:\Users\wddsh\.ssh\id_ed25519.pub +``` + +到: + +```text +C:\Users\wdd\.ssh\ +``` + +- [ ] **Step 3:修正权限** + +在台式机 PowerShell 执行: + +```powershell +icacls "C:\Users\wdd\.ssh\id_ed25519" /inheritance:r +icacls "C:\Users\wdd\.ssh\id_ed25519" /grant:r "$env:USERNAME:F" +``` + +--- + +## 8. Phase 7:Syncthing 自启动与运行参数 + +### Task 28:优先用任务计划程序随用户登录启动 + +**操作位置:** 笔记本和台式机都执行。 + +- [ ] **Step 1:打开任务计划程序** + +按 `Win + R`,输入: + +```text +taskschd.msc +``` + +- [ ] **Step 2:创建任务** + +点击: + +```text +Task Scheduler Library -> Create Task +``` + +General 页填写: + +```text +Name: Syncthing +Run only when user is logged on +``` + +- [ ] **Step 3:设置触发器** + +Triggers 页点击 New: + +```text +Begin the task: At log on +Specific user: 当前用户 +``` + +- [ ] **Step 4:设置动作** + +Actions 页点击 New: + +```text +Action: Start a program +Program/script: C:\Tools\Syncthing\syncthing.exe +Add arguments: --no-console --no-browser --logfile="C:\Tools\Syncthing\syncthing.log" --logflags=3 +Start in: C:\Tools\Syncthing +``` + +> [!TIP] +> `--logfile` 将 Syncthing 运行日志持久化到文件,便于事后排查同步异常。`--logflags=3` 输出日期+时间前缀。日志文件会随运行时间增长,可在每月巡检时检查大小或清空。 + +- [ ] **Step 5:设置条件** + +Conditions 页: + +```text +取消勾选 Start the task only if the computer is on AC power +取消勾选 Stop if the computer switches to battery power +``` + +- [ ] **Step 6:设置运行策略** + +Settings 页: + +```text +勾选 Allow task to be run on demand +勾选 Run task as soon as possible after a scheduled start is missed +取消勾选 Stop the task if it runs longer than +``` + +- [ ] **Step 7:保存并测试** + +保存任务后,右键任务选择: + +```text +Run +``` + +打开: + +```text +http://127.0.0.1:8384 +``` + +预期结果:Syncthing Web UI 可访问。 + +### Task 29:不优先使用 Windows 服务模式 + +本方案不把 NSSM 服务模式作为默认做法。原因: + +- 当前是个人工作站场景,不是无人值守服务器。 +- Syncthing 官方文档也更推荐终端用户随登录启动。 +- 服务模式需要额外处理运行用户、GUI 密码、文件权限和安全边界。 + +只有在台式机长期无人登录但仍需同步时,才考虑 NSSM 服务模式。 + +### Task 29a:配置 Syncthing 全局安全参数 + +**操作位置:** 笔记本和台式机都执行。 + +- [ ] **Step 1:设置最小磁盘剩余空间** + +在 Syncthing Web UI 中打开: + +```text +Actions -> Settings -> General +``` + +找到 `Minimum Free Disk Space`,设置为: + +```text +5 % +``` + +保存。此参数确保磁盘空间不足时 Syncthing 自动暂停同步,防止写满系统盘导致系统异常。 + +- [ ] **Step 2:确认日志文件已生成** + +Task 28 中已通过启动参数 `--logfile` 启用日志持久化。确认日志文件存在: + +```powershell +Test-Path "C:\Tools\Syncthing\syncthing.log" +``` + +预期结果:`True`。 + +如日志文件增长过大,可在巡检时清空: + +```powershell +Clear-Content "C:\Tools\Syncthing\syncthing.log" +``` + +--- + +## 9. Phase 8:监控、巡检与冲突处理 + +### Task 30:每日快速巡检 + +**操作位置:** 任意一台机器。 + +- [ ] **Step 1:打开 Syncthing Web UI** + +访问: + +```text +http://127.0.0.1:8384 +``` + +- [ ] **Step 2:检查三个状态** + +确认: + +```text +Remote Device: Connected +WeChat xwechat_files: Up to Date +IdeaProjects: Up to Date +``` + +如果 Windows Terminal 配置也纳入同步,确认: + +```text +Windows Terminal: Up to Date +``` + +- [ ] **Step 3:检查 Recent Changes** + +点击: + +```text +Recent Changes +``` + +确认没有异常的大批量删除。 + +### Task 31:每周冲突文件检查 + +**操作位置:** 笔记本和台式机。 + +- [ ] **Step 1:查找冲突文件** + +笔记本 PowerShell: + +```powershell +Get-ChildItem "C:\Users\wddsh\xwechat_files","C:\Users\wddsh\Documents\IdeaProjects" -Recurse -Force -Filter "*sync-conflict*" -ErrorAction SilentlyContinue +``` + +台式机 PowerShell: + +```powershell +Get-ChildItem "C:\Users\wdd\xwechat_files","C:\Users\wdd\Documents\IdeaProjects" -Recurse -Force -Filter "*sync-conflict*" -ErrorAction SilentlyContinue +``` + +- [ ] **Step 2:处理 Git 冲突文件** + +如果冲突文件出现在 Git 仓库中: + +1. 打开原文件和 `*.sync-conflict-*` 文件。 +2. 使用 IDE diff 工具比较内容。 +3. 把需要保留的内容合并到原文件。 +4. 删除 `*.sync-conflict-*` 文件。 +5. 执行: + +```powershell +git status +git diff +git add -A +git commit -m "fix: resolve syncthing conflict" +``` + +- [ ] **Step 3:处理微信冲突文件** + +微信数据冲突不要手动改数据库文件。处理规则: + +1. 如果冲突文件是图片、视频、语音等媒体文件,保留较新的或两者都保留。 +2. 如果冲突文件是数据库文件,先停止两端微信和 Syncthing。 +3. 从 `.stversions` 中找出最近正常版本。 +4. 复制出备份后,再决定恢复哪一份。 +5. 不确定时,不删除任何数据库冲突文件。 + +### Task 32:每月容量检查 + +**操作位置:** 两台机器。 + +- [ ] **Step 1:检查 C 盘空间** + +PowerShell: + +```powershell +Get-PSDrive C | Select-Object Name,Used,Free +``` + +- [ ] **Step 2:检查 Syncthing 版本目录** + +笔记本: + +```powershell +Get-ChildItem "C:\Users\wddsh\xwechat_files\.stversions","C:\Users\wddsh\Documents\IdeaProjects\.stversions" -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum +``` + +台式机: + +```powershell +Get-ChildItem "C:\Users\wdd\xwechat_files\.stversions","C:\Users\wdd\Documents\IdeaProjects\.stversions" -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum +``` + +- [ ] **Step 3:空间不足时先降低版本保留天数** + +在 Syncthing Web UI 中进入对应文件夹: + +```text +Edit -> File Versioning -> Maximum Age +``` + +从: + +```text +30 days +``` + +临时调整为: + +```text +14 days +``` + +不要直接删除正在使用中的同步目录。 + +--- + +## 10. Phase 9:手动触发 Syncthing 重新扫描 + +### Task 33:从 Web UI 手动扫描 + +**操作位置:** 任意一端。 + +- [ ] **Step 1:打开文件夹菜单** + +在 Syncthing Web UI 中点击目标文件夹。 + +- [ ] **Step 2:点击 Rescan** + +等待状态从 `Scanning` 变为: + +```text +Up to Date +``` + +### Task 34:准备 PowerShell API 扫描脚本 + +**操作位置:** 两台机器,可选。 + +- [ ] **Step 1:获取 API Key** + +Syncthing Web UI: + +```text +Actions -> Settings -> General -> API Key +``` + +复制 API Key。 + +- [ ] **Step 2:创建脚本目录** + +PowerShell 人工执行: + +```powershell +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\syncthing-scripts" +``` + +- [ ] **Step 3:创建 `rescan-wechat.ps1`** + +用记事本创建: + +```text +%USERPROFILE%\syncthing-scripts\rescan-wechat.ps1 +``` + +写入: + +```powershell +$ErrorActionPreference = "Stop" +$apiKey = Read-Host "Paste this machine's Syncthing API Key" +$headers = @{ "X-API-Key" = $apiKey } +$uri = "http://127.0.0.1:8384/rest/db/scan?folder=wechat-xwechat-files" +Invoke-RestMethod -Method Post -Uri $uri -Headers $headers +``` + +- [ ] **Step 4:创建 `rescan-ideaprojects.ps1`** + +用记事本创建: + +```text +%USERPROFILE%\syncthing-scripts\rescan-ideaprojects.ps1 +``` + +写入: + +```powershell +$ErrorActionPreference = "Stop" +$apiKey = Read-Host "Paste this machine's Syncthing API Key" +$headers = @{ "X-API-Key" = $apiKey } +$uri = "http://127.0.0.1:8384/rest/db/scan?folder=ideaprojects" +Invoke-RestMethod -Method Post -Uri $uri -Headers $headers +``` + +- [ ] **Step 5:手动触发扫描** + +PowerShell 人工执行: + +```powershell +powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\syncthing-scripts\rescan-wechat.ps1" +powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\syncthing-scripts\rescan-ideaprojects.ps1" +``` + +预期结果:返回 JSON 或空响应,Web UI 中对应文件夹进入扫描状态。 + +--- + +## 11. 回滚方案 + +### Task 35:暂停 Syncthing 同步 + +**操作位置:** 两端 Syncthing Web UI。 + +- [ ] **Step 1:暂停远程设备** + +对 Remote Device 点击: + +```text +Pause +``` + +- [ ] **Step 2:暂停问题文件夹** + +对问题文件夹点击: + +```text +Pause +``` + +### Task 36:从预实施备份恢复台式机目录 + +**操作位置:** 台式机。 + +- [ ] **Step 1:恢复微信目录** + +确认 Syncthing 已暂停后,人工执行: + +```powershell +robocopy "C:\Users\wdd\sync-preflight-backup\xwechat_files" "C:\Users\wdd\xwechat_files" /MIR /COPY:DAT /DCOPY:DAT /R:1 /W:1 /XJ +``` + +- [ ] **Step 2:恢复 Git 项目目录** + +确认 Syncthing 已暂停后,人工执行: + +```powershell +robocopy "C:\Users\wdd\sync-preflight-backup\IdeaProjects" "C:\Users\wdd\Documents\IdeaProjects" /MIR /COPY:DAT /DCOPY:DAT /R:1 /W:1 /XJ +``` + +- [ ] **Step 3:删除 Syncthing 文件夹配置** + +在 Syncthing Web UI 中对问题文件夹选择: + +```text +Edit -> Remove +``` + +只移除 Syncthing 配置,不手动删除实际数据目录。 + +### Task 37:从 Syncthing 版本目录恢复单个文件 + +**操作位置:** 文件所在机器。 + +- [ ] **Step 1:打开版本目录** + +微信版本目录通常在: + +```text +C:\Users\wddsh\xwechat_files\.stversions +C:\Users\wdd\xwechat_files\.stversions +``` + +Git 项目版本目录通常在: + +```text +C:\Users\wddsh\Documents\IdeaProjects\.stversions +C:\Users\wdd\Documents\IdeaProjects\.stversions +``` + +- [ ] **Step 2:复制目标版本到临时目录** + +在资源管理器中打开 `.stversions`,按文件名和修改时间找到要恢复的版本,先复制到: + +```powershell +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\restore-review" +``` + +不要剪切,不要直接覆盖原文件。 + +- [ ] **Step 3:人工 diff 后恢复** + +确认内容正确后,再复制回原路径。不要直接在 `.stversions` 中编辑文件。 + +--- + +## 12. 最终验收清单 + +- [ ] 两台机器 Syncthing 都能访问 `http://127.0.0.1:8384`。 +- [ ] 两台机器互相显示 `Connected`,并且不是 Relay 优先连接。 +- [ ] `WeChat xwechat_files` 两端都是 `Up to Date`。 +- [ ] 微信测试文件能笔记本到台式机、台式机到笔记本双向同步。 +- [ ] `IdeaProjects` 两端都是 `Up to Date`。 +- [ ] Git 项目测试文件能双向同步。 +- [ ] `.stignore` 已存在于微信目录和 `IdeaProjects` 根目录。 +- [ ] 微信和 Git 文件夹都启用了 Staggered File Versioning,保留 30 天。 +- [ ] Syncthing 已通过任务计划程序随用户登录启动。 +- [ ] 切换微信登录设备前,已经形成“退出微信 -> 等待 Up to Date -> 另一端登录”的习惯。 +- [ ] 切换 Git 开发机器前,已经形成“commit/stash -> 等待 Up to Date -> 另一端 git status”的习惯。 + +--- + +## 13. 参考依据 + +- 原始方案:`35-黑苹果DELL/7-数据同步备份-高级解决方案.md` +- 微信 rsync 基础脚本:`35-黑苹果DELL/6-微信数据备份.md` +- Syncthing 下载页:`https://syncthing.net/downloads/` +- Syncthing Windows 自启动说明:`https://docs.syncthing.net/users/autostart.html` +- Syncthing 防火墙与直连排查:`https://docs.syncthing.net/users/firewall.html` +- Syncthing 忽略规则:`https://docs.syncthing.net/users/ignoring.html` +- Syncthing 文件版本控制:`https://docs.syncthing.net/users/versioning.html` +- Syncthing 手动扫描 API:`https://docs.syncthing.net/rest/db-scan-post.html` diff --git a/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份-高级解决方案.md b/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份-高级解决方案.md new file mode 100644 index 0000000..eeae80b --- /dev/null +++ b/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份-高级解决方案.md @@ -0,0 +1,328 @@ +# 两台 Windows 工作主机数据同步方案 + +## 1. 背景与环境 + +| 项目 | 笔记本(源) | 台式机(目标) | +|------|-------------|---------------| +| 型号 | 联想 R9000P | 自组装台式机 | +| 系统 | Windows 11 专业版 | Windows 11 LTSC | +| 用户 | `wddsh` | `wdd` | +| 局域网IP | - | `192.168.1.194` | +| WSL2 | 可用 | 已安装 | +| 网络 | 局域网互通,台式机已配置 Clash Verge TUN | + +## 2. 同步场景分析 + +### 场景 A:微信数据(高优先级) +- **路径**: 笔记本 `C:\Users\wddsh\Documents\xwechat_files` / 台式机 `C:\Users\wdd\xwechat_files` +- **特征**: ~30GB,海量小文件(聊天记录、图片、视频、语音、缓存) +- **难点**: 文件数量极多(可能 10 万+),微信运行时数据库锁定,两端都会登录微信产生新数据 +- **频率**: 实时同步(微信关闭后触发最佳) +- **方向**: **双向**(两端都可能产生新聊天记录和文件) + +### 场景 B:Git 项目仓库(高优先级) +- **路径**: 多个项目分布在 `Documents\IdeaProjects` 等目录 +- **特征**: 含 `.git` 目录,大量小文件,已有版本管理 +- **难点**: 需要保持 `.git` 完整性,可能有 node_modules 等超大依赖目录 +- **频率**: 持续双向同步 +- **方向**: 双向 + +### 场景 C:开发工具配置(中优先级) +- **路径**: IDEA 配置在 `%APPDATA%\JetBrains`,VS Code 配置等 +- **特征**: 配置文件体量小,但路径分散 +- **难点**: 部分配置含绝对路径,需要筛选可移植项 +- **频率**: 变动时同步 +- **方向**: 单向或双向 + +--- + +## 3. 方案对比 + +### 方案一:WSL2 rsync(通过 SSH 隧道) + +> [!TIP] +> 你已有一个可用的微信 rsync 脚本([6-微信数据备份.md](file:///c:/Users/wddsh/Documents/IdeaProjects/ProjectAGiPrompt/35-黑苹果DELL/6-微信数据备份.md)),可以复用和扩展。 + +| 维度 | 评价 | +|------|------| +| **海量小文件性能** | ⭐⭐⭐⭐ rsync 增量算法高效,`-z` 压缩减少传输量 | +| **断点续传** | ⭐⭐⭐⭐⭐ `--partial` 原生支持 | +| **双向同步** | ⭐⭐ 原生不支持双向,需要额外脚本或 unison 替代 | +| **自动化** | ⭐⭐⭐⭐ 配合 cron/Task Scheduler 轻松定时 | +| **部署复杂度** | ⭐⭐⭐ 两端都需要 WSL2 + SSH 配置 | +| **文件监控** | ⭐⭐ 需要额外工具(inotifywait/fswatch) | +| **适合场景** | 仅作为首次全量灌入工具,不适合持续双向同步 | + +**关键优势**: +- 增量传输,只传输变化的部分,对 30GB 小文件场景极其高效 +- 你已有成熟的 rsync 脚本基础 +- 可精确控制 `--exclude` 排除规则 + +**关键劣势**: +- 双向同步困难,冲突解决需要手动处理 +- WSL2 访问 Windows 文件系统(`/mnt/c`)有 I/O 性能损耗 +- 非实时,需定时触发 + +--- + +### 方案二:Syncthing(P2P 实时同步) + +| 维度 | 评价 | +|------|------| +| **海量小文件性能** | ⭐⭐⭐ 初次扫描慢,后续增量快;有文件监控加速 | +| **断点续传** | ⭐⭐⭐⭐ 支持 | +| **双向同步** | ⭐⭐⭐⭐⭐ 原生双向,冲突自动检测并保留两个版本 | +| **自动化** | ⭐⭐⭐⭐⭐ 后台服务,文件变化自动触发 | +| **部署复杂度** | ⭐⭐⭐⭐⭐ 图形界面,零配置发现 | +| **文件监控** | ⭐⭐⭐⭐⭐ 内置 fsnotify,变化秒级感知 | +| **适合场景** | A(微信数据双向)、B(Git仓库双向)、C(配置文件双向) | + +**关键优势**: +- 原生双向同步 + 冲突处理 +- 实时文件监控,修改即同步 +- Windows 原生运行,无 WSL 性能损耗 +- Web UI 管理界面,可视化监控同步状态 +- P2P 加密传输,无需第三方服务器 +- 支持 `.stignore` 排除规则(类似 `.gitignore`) + +**关键劣势**: +- 首次同步 30GB 小文件扫描建索引较慢(约 10-30 分钟) +- 不如 rsync 的增量算法精细(rsync 做块级 diff,Syncthing 做文件级 diff) +- 内存占用相对较高(索引占用) + +--- + +### 方案三:Robocopy(Windows 原生) + +| 维度 | 评价 | +|------|------| +| **海量小文件性能** | ⭐⭐⭐ 多线程 `/MT:16` 有帮助,但无增量 diff | +| **断点续传** | ⭐⭐⭐ `/Z` 可恢复模式 | +| **双向同步** | ⭐ 不支持,纯镜像/复制 | +| **自动化** | ⭐⭐⭐⭐ 配合 Task Scheduler | +| **部署复杂度** | ⭐⭐⭐⭐⭐ 系统自带,零安装 | +| **文件监控** | ⭐⭐⭐ `/MON` 参数支持变化监控 | +| **适合场景** | 简单的单向镜像 | + +**关键优势**: +- 系统自带,无需安装任何东西 +- `/MT` 多线程复制,对大量小文件有帮助 +- 通过 SMB 共享直接访问,无需 SSH + +**关键劣势**: +- 无块级增量,每次传输完整变化文件 +- 依赖 SMB 网络共享,需配置防火墙和共享权限 +- 无双向同步能力 + +--- + +### 方案四:Unison(双向 rsync 增强) + +| 维度 | 评价 | +|------|------| +| **海量小文件性能** | ⭐⭐⭐⭐ 基于 rsync 算法 | +| **双向同步** | ⭐⭐⭐⭐⭐ 专为双向设计,冲突检测与合并 | +| **部署复杂度** | ⭐⭐⭐ 两端需要相同版本 | +| **适合场景** | B(Git仓库精确双向) | + +--- + +## 4. 推荐方案:Syncthing 统一管控 + rsync 辅助首次灌入 + +> [!IMPORTANT] +> 三大场景全部需要双向同步,**Syncthing 作为唯一的持续同步引擎**,rsync 仅在首次全量灌入时使用以加速初始化。 + +### 总体架构 + +``` +┌────────────────────────────────────────────────────────────┐ +│ 笔记本 (R9000P, Win11 Pro) │ +│ │ +│ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │ +│ │ 微信数据 │ │ Git 项目仓库 │ │ 开发工具配置 │ │ +│ │ 30GB │ │ 多个 repos │ │ IDEA/VSCode 等 │ │ +│ └────┬─────┘ └──────┬───────┘ └────────┬──────────┘ │ +│ │ │ │ │ +│ └───────────────┼───────────────────┘ │ +│ │ │ +│ ┌────────┴────────┐ │ +│ │ Syncthing │ │ +│ │ (统一双向实时) │ │ +│ └────────┬────────┘ │ +│ │ │ +└───────────────────────┼────────────────────────────────────┘ + │ + P2P 加密传输 + (局域网) + │ +┌───────────────────────┼────────────────────────────────────┐ +│ │ │ +│ ┌────────┴────────┐ │ +│ │ Syncthing │ │ +│ │ (统一双向实时) │ │ +│ └────────┬────────┘ │ +│ ┌───────────────┼───────────────────┐ │ +│ │ │ │ │ +│ ┌────┴─────┐ ┌──────┴───────┐ ┌────────┴──────────┐ │ +│ │ 微信数据 │ │ Git 项目仓库 │ │ 开发工具配置 │ │ +│ │ 双向同步 │ │ 双向同步 │ │ 双向同步 │ │ +│ └──────────┘ └──────────────┘ └───────────────────┘ │ +│ │ +│ 台式机 (Win11 LTSC) │ +└────────────────────────────────────────────────────────────┘ + +首次全量灌入(可选加速): + 笔记本 WSL2 ──rsync──▶ 台式机 WSL2 (一次性,完成后切换到 Syncthing) +``` + +--- + +### 场景 A:微信数据 → Syncthing(双向实时)+ rsync 首次灌入 + +> [!WARNING] +> **微信数据双向同步的核心风险:** 微信不支持同一账号在两台 PC 同时登录。因此「双向同步」的实际含义是——你在哪台机器上登录微信,新产生的聊天数据就从那台同步到另一台。两端不会同时产生新数据,冲突风险较低。 + +**为什么选 Syncthing:** +- 双向同步是硬需求,rsync 无法满足 +- 微信数据以追加为主(新聊天、新图片),Syncthing 的文件级增量足够高效 +- 实时文件监控,切换登录设备后新数据自动同步 +- Windows 原生运行,无 WSL I/O 损耗 + +**为什么首次灌入用 rsync:** +- 30GB 海量小文件的首次传输,rsync 的块级增量 + 压缩 + 断点续传最稳定 +- Syncthing 首次扫描建索引 + 传输 30GB 小文件可能需要数小时 +- 用 rsync 先把数据灌到台式机,再让 Syncthing 「发现」已有数据建索引,速度大幅提升 + +**实现要点:** +1. **首次灌入**:复用已有 rsync 脚本([6-微信数据备份.md](file:///c:/Users/wddsh/Documents/IdeaProjects/ProjectAGiPrompt/35-黑苹果DELL/6-微信数据备份.md)),将笔记本微信数据全量推送到台式机 +2. **持续同步**:两端 Syncthing 配置微信数据目录为共享文件夹,类型设为 **Send & Receive**(双向) +3. **排除规则**(`.stignore`): + +``` +// .stignore - 微信数据排除规则 +temp +cache +apm_record +*.lock +*.tmp +crash_report +log +``` + +4. **数据库保护策略**: + - 同步前确保微信已关闭(避免 db 锁定导致同步损坏文件) + - 启用 Syncthing 的 **Staggered File Versioning**(阶梯式版本保留),保留最近 30 天的版本 + - 关键目录 `db_storage` 建议在微信运行时通过 `.stignore` 临时排除,关闭微信后恢复同步 + +5. **同步触发优化**: + - Syncthing 默认使用 fsnotify 实时监控,但微信关闭时会批量释放文件锁 + - 配置 `rescanIntervalS` 为 300(5分钟全量扫描兜底) + - 可配合脚本在检测到微信关闭时手动触发 Syncthing API 重新扫描 + +> [!TIP] +> **微信只能单端PC登录**的特性反而降低了双向同步的冲突风险。实际上是一种「跟随主力设备」的同步模式:你在哪台机器工作,新数据就从哪台流向另一台。 + +--- + +### 场景 B:Git 项目仓库 → Syncthing(双向实时) + +**为什么选 Syncthing:** +- 项目仓库需要双向同步(两台机器都可能编辑) +- 实时文件监控,代码修改即同步 +- `.stignore` 可以排除 `node_modules`、`build` 等目录 +- 不破坏 `.git` 完整性 + +**实现要点:** +1. 两端安装 Syncthing(Windows 原生版本) +2. 同步 `Documents\IdeaProjects` 整个目录 +3. 配置 `.stignore` 精确排除: + +``` +// .stignore 排除规则 +node_modules +.gradle +build +dist +target +*.class +*.jar +*.pyc +__pycache__ +.idea/workspace.xml +.idea/tasks.xml +.idea/usage.statistics.xml +.idea/shelf +*.iws +``` + +4. 设置冲突处理策略:默认保留两个版本,手动解决 +5. 设置为 Windows 服务开机自启 + +> [!WARNING] +> **Git 仓库同步注意事项:** +> - 避免两端**同时编辑同一文件**,否则会产生冲突文件(`*.sync-conflict-*`) +> - 建议在同一时间只在一端进行开发,另一端作为同步镜像 +> - 如果需要真正的多端协作,应通过 Git remote(如 GitHub/GitLab)push/pull,而非文件同步 + +--- + +### 场景 C:开发工具配置 → Syncthing + JetBrains Settings Sync + +**分层策略:** + +| 工具 | 同步方式 | 说明 | +|------|---------|------| +| IDEA / WebStorm 等 | JetBrains Settings Sync | 内置云同步,登录 JetBrains 账号即可 | +| VS Code | Settings Sync | 内置,登录 GitHub/Microsoft 账号 | +| 其他散落配置 | Syncthing 同步指定目录 | `%APPDATA%` 下的特定子目录 | +| Windows Terminal 配置 | Syncthing | `settings.json` 文件同步 | +| SSH 配置 | 手动复制 | `~/.ssh/config` 一次性迁移 | + +**实现要点:** +1. IDEA 和 VS Code 优先使用内置的 Settings Sync(最稳定、无兼容性问题) +2. 对于没有内置同步的工具,用 Syncthing 同步其配置目录 +3. SSH 配置、Git 全局配置等一次性手动复制 + +--- + +## 5. 部署顺序 + +| 阶段 | 内容 | 预计耗时 | +|------|------|---------| +| **Phase 1** | 两端 WSL2 环境搭建 + SSH 免密配置(为 rsync 首次灌入做准备) | 30 分钟 | +| **Phase 2** | rsync 微信数据首次全量灌入(笔记本 → 台式机) | 1-2 小时 | +| **Phase 3** | 两端安装 Syncthing + 配置三个共享文件夹(微信/Git/配置) | 1 小时 | +| **Phase 4** | Syncthing 首次索引建立 + 验证双向同步 | 30-60 分钟 | +| **Phase 5** | 开发工具内置 Settings Sync 启用(IDEA/VSCode) | 15 分钟 | +| **Phase 6** | Syncthing 设为 Windows 服务开机自启 + 监控脚本 | 30 分钟 | + +--- + +## 6. 安全与容错 + +1. **首次同步前全量备份**:在台式机上先做一次完整的本地备份 +2. **rsync 首次灌入使用 `--dry-run`**:先预览,确认无误再执行 +3. **Syncthing 版本控制**:所有共享文件夹启用 Staggered File Versioning(阶梯式版本保留) + - 最近 1 小时:保留所有版本 + - 最近 1 天:每小时保留一个版本 + - 最近 30 天:每天保留一个版本 +4. **微信数据库保护**:同步脚本检测 `Weixin.exe` 进程,运行时临时跳过 `db_storage` 目录 +5. **日志审计**:Syncthing Web UI 自带同步日志,额外配置日志输出到文件 +6. **磁盘空间监控**:Syncthing 配置 `minDiskFree` 参数,磁盘空间不足时自动暂停同步 +7. **冲突文件清理**:定期检查并清理 `*.sync-conflict-*` 文件 + +--- + +## 用户审阅结果(已确认 ✅) + +| # | 审阅项 | 用户确认 | +|---|--------|----------| +| 1 | Git 仓库同步目录 | ✅ 仅 `Documents\IdeaProjects`,无其他目录 | +| 2 | 台式机微信数据路径 | ✅ `C:\Users\wdd\xwechat_files` | +| 3 | NAS / 外部存储 | ✅ 不考虑 | +| 4 | 台式机 WSL2 | ✅ 已安装完成 | +| 5 | 台式机磁盘空间 | ✅ 充足 | +| 6 | 采用方案 | ✅ **Syncthing 统一管控 + rsync 辅助首次灌入** | + +> [!NOTE] +> 所有审阅项已确认,方案可以进入实施阶段。 diff --git a/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份.md b/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份.md new file mode 100644 index 0000000..b6727bb --- /dev/null +++ b/35-黑苹果DELL/2-工作电脑备份/7-数据同步备份.md @@ -0,0 +1,17 @@ +背景说明: +1. 我现在windows11专业版的联想R9000P笔记本电脑 +2. 我重新安装了 windows 11 ltsc的台式机电脑,现在想把工作的主力迁移到台式机电脑 +3. 我拥有丰富的各类系统的操作经验,有大量的linux服务器,windows服务器资源,可以执行任何方案 + +我现在写的需求是两台工作主机之间很多数据是需要互相同步的 + +1. 例如 微信的聊天数据、文件数据等等,30GB的小文件,超多的文件 +2. 开发项目仓库,我用了git进行版本管理,但是有很多项目仓库,我需要全部同步 +3. IDEA等开发工具的配置等同步 + + +1. 你需要研究高效、大量、涉及很多小文件的数据同步机 +2. 方案一:WSL2下面的LINUX系统之间使用RSYNC同步 +2. 方案二:借助syncthing等外部工具 + +其他高效的方案 \ No newline at end of file diff --git a/35-黑苹果DELL/3-WSL系统优化/1-WSL终端优化-prompt.md b/35-黑苹果DELL/3-WSL系统优化/1-WSL终端优化-prompt.md new file mode 100644 index 0000000..184ca94 --- /dev/null +++ b/35-黑苹果DELL/3-WSL系统优化/1-WSL终端优化-prompt.md @@ -0,0 +1,20 @@ +我在windows 11 LTSC安装了 WSL2的ubuntu 24.04的系统 + +## 网络环境 +1. 电脑和WSL位于中国大陆境内 +2. windows开启了TUN模式的clash verge +3. 需要考虑国外工具的下载时候使用代理 +4. 电脑的IP地址为192.168.1.20 + +## 核心需求 +1. 提供功能函数组合的工具集shell脚本,将WSL打造为开发工作中心 +2. 镜像加速源头,例如清华源等国内速度快的源 +3. 终端需要修改为强于oh my zsh的终端,并且安装丰富好用的插件 +4. 终端的字体需要支持oh my zsh带来的特殊字体,终端显示unicode不能出现乱码 +5. 需要安装curl git wget telnet ping mtr等常用工具 +6. 安装golang的最新版本,并设置/bin/bash /bin/zsh /bin/sh 和 自定义终端工具的环境变量等 +7. 安装nodejs-lts的最新版本,并设置/bin/bash /bin/zsh /bin/sh 和 自定义终端工具的环境变量等 + +## 复用性 +1. 需要考虑并制作一个高可用可复用的shell脚本 +2. 支持ubuntu 20.04 22.04 24.04 26.04的系统,能够判断系统版本,然后进行调整 \ No newline at end of file diff --git a/35-黑苹果DELL/4-ubuntu26.04-desktop工作优化.md b/35-黑苹果DELL/3-WSL系统优化/2-ubuntu26.04-desktop-prompt.md similarity index 100% rename from 35-黑苹果DELL/4-ubuntu26.04-desktop工作优化.md rename to 35-黑苹果DELL/3-WSL系统优化/2-ubuntu26.04-desktop-prompt.md diff --git a/35-黑苹果DELL/3-WSL系统优化/3.1-wsl初始化脚本-实现计划.md b/35-黑苹果DELL/3-WSL系统优化/3.1-wsl初始化脚本-实现计划.md new file mode 100644 index 0000000..1262ddd --- /dev/null +++ b/35-黑苹果DELL/3-WSL系统优化/3.1-wsl初始化脚本-实现计划.md @@ -0,0 +1,256 @@ +# WSL2 开发环境一键配置脚本 + +将 WSL2 Ubuntu 打造为功能完备的开发工作中心。单文件模块化 Shell 脚本,支持 Ubuntu 20.04/22.04/24.04/26.04,幂等可重复执行。 + +## User Review Required + +> [!IMPORTANT] +> **脚本文件位置**: 计划放置于 `rmdc-continuous-integration/scripts/wsl/wsl-dev-setup.sh`,如有其他偏好请指出。 + +> [!IMPORTANT] +> **终端方案选择**: 采用 **Zinit + Starship** 组合替代 oh-my-zsh: +> - **Zinit**:现代 Zsh 插件管理器,支持 Turbo 模式延迟加载,启动速度远超 oh-my-zsh +> - **Starship**:Rust 编写的跨 Shell 提示符,极快且美观,原生支持 Nerd Font 图标 +> - 如更偏好 **Powerlevel10k**(纯 Zsh、瞬时启动),请告知 + +> [!IMPORTANT] +> **Node.js 管理器**: 采用 **fnm** (Fast Node Manager,Rust 编写) 替代 nvm,速度快 40x。如偏好 nvm 请告知。 + +## Open Questions + +> [!NOTE] +> **代理模式**:Clash Verge TUN 模式下,WSL2 流量通常已自动代理。脚本默认 **不设置** 显式 `HTTP_PROXY` 环境变量,但提供 `--proxy` 开关可手动启用(用于 TUN 未覆盖 WSL 的场景)。是否需要默认启用? + +## Proposed Changes + +### 脚本架构 + +单文件 `wsl-dev-setup.sh`,约 1000+ 行,函数式模块化设计: + +``` +wsl-dev-setup.sh +├─ 📋 全局配置区(环境变量、默认值) +├─ 🎨 日志与颜色(log_info/warn/error/success,进度条) +├─ 🔧 工具函数(run_as_user, backup_file, download_file, add_to_rc) +├─ 🖥️ 系统检测(Ubuntu 版本、架构、WSL 检测) +├─ 🌐 代理配置(Clash Verge 可选显式代理) +├─ 📦 APT 镜像源(清华/中科大/阿里云,自动适配 DEB822 格式) +├─ 🌍 Locale 配置(en_US.UTF-8 + zh_CN.UTF-8) +├─ 🛠️ 基础工具安装(curl/git/wget/telnet/ping/mtr 等) +├─ 💻 Zsh + Zinit + Starship(终端环境) +├─ 🔤 Nerd Font 安装(JetBrains Mono Nerd Font) +├─ ⚡ 现代 CLI 工具(bat/eza/fd/ripgrep/fzf/zoxide/delta/lazygit) +├─ 🐹 Go 最新版安装(自动检测最新版本 + goproxy.cn) +├─ 📗 Node.js LTS 安装(fnm + npmmirror.com) +├─ 📊 安装总结与验证 +└─ 🚀 Main(参数解析、模块调度) +``` + +--- + +### [NEW] [wsl-dev-setup.sh](file:///c:/Users/wdd/Documents/IdeaProjects/RMDC-CLOUD/rmdc-continuous-integration/scripts/wsl/wsl-dev-setup.sh) + +#### 1. 命令行接口 + +```bash +# 完整安装(推荐) +sudo bash wsl-dev-setup.sh + +# 查看帮助 +bash wsl-dev-setup.sh --help + +# 仅安装指定模块 +sudo bash wsl-dev-setup.sh --only golang,nodejs + +# 跳过指定模块 +sudo bash wsl-dev-setup.sh --skip fonts,modern-cli + +# 启用显式代理(TUN 未覆盖 WSL 时) +sudo bash wsl-dev-setup.sh --proxy + +# 自定义配置 +sudo WINDOWS_HOST_IP=192.168.1.100 CLASH_PROXY_PORT=7891 bash wsl-dev-setup.sh +``` + +可选模块名:`proxy | mirror | locale | base-tools | zsh | fonts | modern-cli | golang | nodejs` + +#### 2. 系统检测与版本适配 + +| Ubuntu 版本 | APT 格式 | 特殊处理 | +|------------|----------|---------| +| 20.04 Focal | `sources.list` 传统格式 | 部分工具需从 GitHub Release 安装(eza/bat 等 apt 版本过旧) | +| 22.04 Jammy | `sources.list` 传统格式 | 大部分工具可直接 apt 安装 | +| 24.04 Noble | DEB822 `.sources` 格式 | 原生支持新格式,apt 源更丰富 | +| 26.04 | DEB822 `.sources` 格式 | 动态检测 codename,前向兼容 | + +版本检测逻辑: +- 读取 `/etc/os-release` 获取 `VERSION_ID` 和 `VERSION_CODENAME` +- 校验是否为 WSL 环境(`/proc/version` 包含 `microsoft`) +- 检测架构(amd64/arm64) + +#### 3. APT 镜像源配置 + +支持三大镜像源(默认清华 TUNA): + +| 镜像源 | URL | +|--------|-----| +| 清华 TUNA | `mirrors.tuna.tsinghua.edu.cn` | +| 中科大 USTC | `mirrors.ustc.edu.cn` | +| 阿里云 | `mirrors.aliyun.com` | + +- **Ubuntu < 24.04**:修改 `/etc/apt/sources.list`,备份原文件 +- **Ubuntu >= 24.04**:修改 `/etc/apt/sources.list.d/ubuntu.sources`(DEB822 格式) +- 自动启用 `main restricted universe multiverse` 四个仓库 + +#### 4. 基础工具安装 + +``` +核心网络工具:curl wget git net-tools inetutils-telnet iputils-ping mtr-tiny dnsutils traceroute +编译工具链: build-essential gcc g++ make cmake +系统工具: ca-certificates gnupg lsb-release software-properties-common +压缩工具: unzip zip tar gzip bzip2 xz-utils p7zip-full +实用工具: jq tree htop ncdu openssh-client tmux +``` + +#### 5. Zsh + Zinit + Starship 终端 + +**Zinit 插件列表**: + +| 插件 | 功能 | 加载方式 | +|------|------|---------| +| `zsh-autosuggestions` | Fish 风格命令建议 | Turbo 延迟加载 | +| `zsh-syntax-highlighting` | 命令语法高亮 | Turbo 延迟加载 | +| `zsh-completions` | 额外补全规则 | Turbo 延迟加载 | +| `fzf-tab` | fzf 驱动的 Tab 补全 | Turbo 延迟加载 | +| `history-substring-search` | 历史子串搜索 | Turbo 延迟加载 | +| OMZ::lib/history.zsh | oh-my-zsh 历史配置 | Snippet | +| OMZ::lib/key-bindings.zsh | oh-my-zsh 快捷键 | Snippet | +| OMZ::lib/completion.zsh | oh-my-zsh 补全配置 | Snippet | +| OMZ::plugins/git | Git 快捷别名 | Snippet | +| OMZ::plugins/sudo | 双击 ESC 添加 sudo | Snippet | +| OMZ::plugins/extract | 万能解压命令 | Snippet | +| OMZ::plugins/z | 智能目录跳转 | Snippet | + +**Starship 配置**: +- 预设美观主题(Nerd Font 图标丰富) +- 显示:Git 分支/状态、Go 版本、Node.js 版本、执行时间、错误码 +- 自定义 `~/.config/starship.toml` + +**Zsh 增强配置**: +- 历史记录:50000 条,去重、共享 +- 补全:菜单选择、大小写不敏感、模糊匹配 +- 常用别名:`ll`, `la`, `..`, `...`, `cls`, `ports` 等 + +#### 6. Nerd Font 安装 + +- 下载 **JetBrains Mono Nerd Font**(从 GitHub Release) +- 安装到 `~/.local/share/fonts/`(Linux 侧) +- 同时复制到 `/mnt/c/Users//AppData/Local/Microsoft/Windows/Fonts/`(Windows 侧,供 Windows Terminal 使用) +- 运行 `fc-cache -fv` 刷新字体缓存 +- 输出 Windows Terminal 字体配置提示 + +#### 7. 现代 CLI 工具 + +| 工具 | 替代 | 安装方式 | +|------|------|---------| +| `bat` | cat | apt (22.04+) / GitHub Release (20.04) | +| `eza` | ls | apt (24.04+) / GitHub Release (旧版) | +| `fd-find` | find | apt | +| `ripgrep` | grep | apt | +| `fzf` | — | apt / GitHub Release | +| `zoxide` | cd/z | apt (24.04+) / GitHub Release | +| `delta` | diff | GitHub Release | +| `lazygit` | git TUI | GitHub Release | +| `tldr` | man | npm (Node.js 安装后) | + +#### 8. Go 最新版安装 + +- 自动从 `https://go.dev/dl/?mode=json` 检测最新稳定版 +- 下载 `go.linux-.tar.gz` 并安装到 `/usr/local/go` +- 创建 `$HOME/go/{bin,src,pkg}` 目录 + +**环境变量配置**(写入以下所有位置): + +| 文件 | 说明 | +|------|------| +| `/etc/profile.d/golang.sh` | 系统级,所有用户生效 | +| `~/.bashrc` | Bash 交互式 Shell | +| `~/.zshrc` | Zsh 交互式 Shell | +| `~/.profile` | 登录 Shell(sh/bash) | + +```bash +export GOROOT=/usr/local/go +export GOPATH=$HOME/go +export PATH=$GOROOT/bin:$GOPATH/bin:$PATH +export GOPROXY=https://goproxy.cn,direct +export GONOSUMDB=* +``` + +#### 9. Node.js LTS 安装 + +- 安装 **fnm**(Fast Node Manager)从 GitHub Release +- 安装 Node.js 最新 LTS 版本 +- 设置 npm 中国镜像:`registry.npmmirror.com` + +**环境变量配置**(写入以下所有位置): + +| 文件 | 说明 | +|------|------| +| `~/.bashrc` | `eval "$(fnm env --shell bash)"` | +| `~/.zshrc` | `eval "$(fnm env --shell zsh)"` | +| `~/.profile` | `eval "$(fnm env)"` | + +#### 10. 代理配置(可选) + +当 `--proxy` 启用时: + +```bash +export http_proxy="http://192.168.1.20:7890" +export https_proxy="http://192.168.1.20:7890" +export no_proxy="localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8" +``` + +- 写入 `~/.bashrc`、`~/.zshrc`、`~/.profile` +- 配置 `git config --global http.proxy` 和 `https.proxy` +- 提供 `proxy_on` / `proxy_off` 快捷函数 + +--- + +## 设计决策 + +| 决策点 | 选择 | 理由 | +|--------|------|------| +| 插件管理器 | Zinit | Turbo 延迟加载,启动 < 100ms,远快于 oh-my-zsh(通常 500ms+) | +| 提示符 | Starship | Rust 编写极快,跨 Shell(bash/zsh 都能用),丰富图标 | +| Node.js 管理 | fnm | Rust 编写,比 nvm 快 40x,支持 `.node-version` / `.nvmrc` | +| 字体 | JetBrains Mono Nerd | 编程利器,包含 6000+ 图标,Ligature 支持 | +| 镜像源 | 清华 TUNA(默认) | 国内速度最快最稳定的开源镜像之一 | +| APT 格式 | 自动适配 | 24.04+ 用 DEB822,旧版用 sources.list | +| Go 代理 | goproxy.cn | 国内最快的 Go 模块代理 | +| npm 镜像 | npmmirror.com | 淘宝 npm 镜像,国内标配 | + +## Verification Plan + +### 脚本内置验证 + +脚本执行完毕后自动运行验证检查,输出安装总结表: + +``` +✅ Ubuntu 24.04 (noble) - amd64 +✅ APT Mirror: mirrors.tuna.tsinghua.edu.cn +✅ Locale: en_US.UTF-8 +✅ Base tools: curl git wget telnet ping mtr ✓ +✅ Zsh 5.9 + Zinit ✓ +✅ Starship v1.x.x ✓ +✅ JetBrains Mono Nerd Font ✓ +✅ Go 1.24.x ✓ (GOROOT=/usr/local/go) +✅ Node.js v22.x.x ✓ (fnm) +✅ npm registry: registry.npmmirror.com +``` + +### Manual Verification + +1. 重新打开 WSL 终端,确认 Zsh + Starship 启动正常 +2. Windows Terminal 中设置字体为 "JetBrainsMono Nerd Font" +3. 验证 `go version`、`node --version`、`npm --version` 输出 +4. 验证 `bat`、`eza`、`fzf`、`rg` 等工具可用 diff --git a/35-黑苹果DELL/3-WSL系统优化/3.2-wsl初始化脚本-实现内容.md b/35-黑苹果DELL/3-WSL系统优化/3.2-wsl初始化脚本-实现内容.md new file mode 100644 index 0000000..d0e3176 --- /dev/null +++ b/35-黑苹果DELL/3-WSL系统优化/3.2-wsl初始化脚本-实现内容.md @@ -0,0 +1,124 @@ +# WSL2 Development Environment Setup + +## 📁 Created Files + +| File | Lines | Description | +|------|-------|-------------| +| [wsl-dev-setup.sh](file:///c:/Users/wdd/Documents/IdeaProjects/RMDC-CLOUD/rmdc-continuous-integration/scripts/wsl/wsl-dev-setup.sh) | 1541 | 一键配置脚本(单文件,模块化函数设计) | + +## 🏗️ Script Architecture + +```mermaid +graph TD + A["main()"] --> B["detect_system()"] + B --> C{"Module Dispatcher"} + C --> D["setup_proxy()"] + C --> E["setup_apt_mirror()"] + C --> F["setup_locale()"] + C --> G["install_base_tools()"] + C --> H["install_zsh_terminal()"] + C --> I["install_nerd_fonts()"] + C --> J["install_modern_cli()"] + C --> K["install_golang()"] + C --> L["install_nodejs()"] + C --> M["print_summary()"] + + H --> H1["_generate_zshrc()"] + H --> H2["_generate_p10k()"] + + J --> J1["bat · eza · fd · rg"] + J --> J2["fzf · zoxide · delta · lazygit"] + + K --> K1["_setup_go_env()"] + L --> L1["_setup_fnm_env()"] +``` + +## 📦 Module Details + +### 1. Proxy (Clash Verge) +- 设置 `http_proxy`, `https_proxy`, `all_proxy` 环境变量 +- 提供 `proxy_on` / `proxy_off` 快捷函数 +- 自动写入 `.bashrc`, `.zshrc`, `.profile` +- 配置 Git 代理 + +### 2. APT Mirror +- **< 24.04**: 传统 `sources.list` 格式 +- **≥ 24.04**: DEB822 `.sources` 格式 +- 支持: 清华 TUNA / 中科大 USTC / 阿里云 + +### 3. Terminal: Zsh + Zinit + Powerlevel10k +- **Zinit** Turbo 延迟加载:启动 < 100ms +- **Powerlevel10k** Rainbow 风格:Powerline 箭头分隔 + 彩色背景段 +- **Instant Prompt**: 终端瞬时响应 +- **Transient Prompt**: 历史命令显示简化 +- **插件**: fast-syntax-highlighting, autosuggestions, completions, fzf-tab, history-substring-search, OMZ git/sudo/extract snippets + +### 4. Go + Node.js +- Go: 自动检测最新版,`golang.google.cn` 镜像下载,`goproxy.cn` 模块代理 +- Node.js: **fnm** (Rust 编写,比 nvm 快 40x),npm 镜像 `npmmirror.com` +- 环境变量写入: `/etc/profile.d/`, `.bashrc`, `.profile`, `.zshrc` + +### 5. Modern CLI Tools + +| Tool | Replaces | Source | +|------|----------|--------| +| bat | cat | apt (22.04+) / GitHub .deb | +| eza | ls | apt (24.04+) / GitHub tar | +| fd | find | apt / GitHub .deb | +| ripgrep | grep | apt / GitHub .deb | +| fzf | — | apt / git clone | +| zoxide | cd | apt (24.04+) / GitHub .deb | +| delta | diff | GitHub .deb + git config | +| lazygit | git TUI | GitHub tar | + +## 🚀 Usage + +### Full Installation +```bash +sudo bash wsl-dev-setup.sh +``` + +### Selective Modules +```bash +# Only install Go and Node.js +sudo bash wsl-dev-setup.sh --only golang,nodejs + +# Skip fonts and modern CLI +sudo bash wsl-dev-setup.sh --skip fonts,modern-cli + +# Use USTC mirror +sudo bash wsl-dev-setup.sh --mirror ustc + +# Disable proxy +sudo bash wsl-dev-setup.sh --no-proxy + +# Preview (dry run) +bash wsl-dev-setup.sh --dry-run +``` + +### Custom Configuration +```bash +# Environment variables +sudo WINDOWS_HOST_IP=10.0.0.1 CLASH_PROXY_PORT=7891 bash wsl-dev-setup.sh +sudo GO_VERSION=1.23.0 bash wsl-dev-setup.sh +sudo NERD_FONT=FiraCode bash wsl-dev-setup.sh +``` + +## ✅ Validation + +- [x] Bash syntax check (`bash -n`): **PASSED** +- [x] 1541 lines, modular function design +- [x] Idempotent: safe to run multiple times +- [x] Error isolation: module failures don't block other modules +- [x] Supports: Ubuntu 20.04 / 22.04 / 24.04 / 26.04 +- [x] Supports: amd64 / arm64 architectures + +## ⚠️ Post-Install Notes + +> [!IMPORTANT] +> **Windows Terminal 字体设置**: 安装完成后需在 Windows Terminal 中手动设置字体为 `JetBrainsMono Nerd Font`,否则终端图标会显示为方框。 +> +> Settings → Profiles → Defaults → Appearance → Font face + +> [!TIP] +> 首次进入 Zsh 后,如对预设 Powerlevel10k 主题不满意,可运行 `p10k configure` 重新配置。 diff --git a/35-黑苹果DELL/3-WSL系统优化/wsl-dev-setup.sh b/35-黑苹果DELL/3-WSL系统优化/wsl-dev-setup.sh new file mode 100644 index 0000000..a8be5ae --- /dev/null +++ b/35-黑苹果DELL/3-WSL系统优化/wsl-dev-setup.sh @@ -0,0 +1,1958 @@ +#!/usr/bin/env bash +#=============================================================================== +# +# WSL2 Development Environment Setup Script +# +# Description : One-click setup to transform WSL2 Ubuntu into a full +# development workstation with modern terminal, languages, +# and CLI tools. +# +# Supports : Ubuntu 20.04 / 22.04 / 24.04 / 26.04 +# Terminal : Zsh + Zinit (plugin manager) + Powerlevel10k (prompt) +# Languages : Go (latest) + Node.js LTS (via fnm) +# Proxy : Clash Verge TUN mode compatible +# +# Usage : sudo bash wsl-dev-setup.sh [OPTIONS] +# sudo bash wsl-dev-setup.sh --help +# +# Version : 1.0.0 +# License : MIT +# +#=============================================================================== + +set -uo pipefail + +#=============================================================================== +# Global Configuration +#=============================================================================== +readonly SCRIPT_VERSION="1.0.0" +readonly SCRIPT_NAME="$(basename "$0")" +readonly SCRIPT_START_TIME="$(date +%s)" + +# --- Network / Proxy --- +WINDOWS_HOST_IP="${WINDOWS_HOST_IP:-192.168.1.20}" +CLASH_PROXY_PORT="${CLASH_PROXY_PORT:-7890}" +ENABLE_PROXY="${ENABLE_PROXY:-true}" + +# --- User --- +INSTALL_USER="${SUDO_USER:-$(whoami)}" +USER_HOME="$(eval echo ~"${INSTALL_USER}" 2>/dev/null || echo "/home/${INSTALL_USER}")" + +# --- Installation Versions --- +GO_VERSION="${GO_VERSION:-latest}" +NODE_VERSION="${NODE_VERSION:-lts}" +NERD_FONT="${NERD_FONT:-JetBrainsMono}" + +# --- APT Mirror --- +APT_MIRROR="${APT_MIRROR:-tuna}" # tuna | ustc | aliyun | default + +# --- Detected at runtime --- +UBUNTU_VERSION="" +UBUNTU_CODENAME="" +ARCH="" +GO_ARCH="" +IS_WSL=false +WIN_USER="" + +# --- Module control --- +ALL_MODULES="proxy mirror locale base-tools zsh fonts modern-cli golang nodejs" +SKIP_MODULES="" +ONLY_MODULES="" +FAILED_MODULES="" + +#=============================================================================== +# Colors & Logging +#=============================================================================== +if [[ -t 1 ]]; then + readonly C_RED='\033[0;31m' + readonly C_GREEN='\033[0;32m' + readonly C_YELLOW='\033[0;33m' + readonly C_BLUE='\033[0;34m' + readonly C_MAGENTA='\033[0;35m' + readonly C_CYAN='\033[0;36m' + readonly C_WHITE='\033[1;37m' + readonly C_BOLD='\033[1m' + readonly C_DIM='\033[2m' + readonly C_NC='\033[0m' +else + readonly C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' + readonly C_MAGENTA='' C_CYAN='' C_WHITE='' C_BOLD='' C_DIM='' C_NC='' +fi + +log_info() { echo -e "${C_BLUE}[INFO]${C_NC} $*"; } +log_success() { echo -e "${C_GREEN}[ OK ]${C_NC} $*"; } +log_warn() { echo -e "${C_YELLOW}[WARN]${C_NC} $*"; } +log_error() { echo -e "${C_RED}[ERROR]${C_NC} $*"; } +log_step() { echo -e "${C_MAGENTA} ➜${C_NC} $*"; } + +section_header() { + local title="$1" + local width=58 + echo "" + echo -e "${C_CYAN}┌$(printf '─%.0s' $(seq 1 $width))┐${C_NC}" + printf "${C_CYAN}│${C_NC} %-$((width - 2))s${C_CYAN}│${C_NC}\n" "$title" + echo -e "${C_CYAN}└$(printf '─%.0s' $(seq 1 $width))┘${C_NC}" + echo "" +} + +print_banner() { + echo "" + echo -e "${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_NC}" + echo -e "${C_CYAN}║${C_NC} ${C_BOLD}🚀 WSL2 Development Environment Setup${C_NC} v${SCRIPT_VERSION} ${C_CYAN}║${C_NC}" + echo -e "${C_CYAN}║${C_NC} ${C_DIM}Zinit + Powerlevel10k · Go · Node.js · Modern CLI${C_NC} ${C_CYAN}║${C_NC}" + echo -e "${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_NC}" + echo "" +} + +#=============================================================================== +# Usage / Help +#=============================================================================== +print_usage() { + cat << USAGE +${C_BOLD}WSL2 Development Environment Setup Script${C_NC} v${SCRIPT_VERSION} + +${C_BOLD}USAGE:${C_NC} + sudo bash ${SCRIPT_NAME} [OPTIONS] + +${C_BOLD}OPTIONS:${C_NC} + -h, --help Show this help message + -v, --version Show script version + --only Only run specified modules (comma-separated) + --skip Skip specified modules (comma-separated) + --no-proxy Disable proxy configuration + --proxy-ip Windows host IP (default: 192.168.1.20) + --proxy-port Clash proxy port (default: 7890) + --mirror APT mirror: tuna|ustc|aliyun|default (default: tuna) + --go-version Go version (default: latest) + --node-version Node.js version (default: lts) + --font Nerd Font name (default: JetBrainsMono) + --dry-run Show what would be done without executing + +${C_BOLD}MODULES:${C_NC} + proxy Proxy configuration (Clash Verge) + mirror APT mirror source configuration + locale Locale & UTF-8 setup + base-tools Essential system tools + zsh Zsh + Zinit + Powerlevel10k + fonts Nerd Font installation + modern-cli Modern CLI replacements (bat, eza, fzf, etc.) + golang Go programming language + nodejs Node.js via fnm + +${C_BOLD}EXAMPLES:${C_NC} + # Full installation (recommended) + sudo bash ${SCRIPT_NAME} + + # Install only Go and Node.js + sudo bash ${SCRIPT_NAME} --only golang,nodejs + + # Skip fonts and modern CLI tools + sudo bash ${SCRIPT_NAME} --skip fonts,modern-cli + + # Use USTC mirror with custom proxy IP + sudo bash ${SCRIPT_NAME} --mirror ustc --proxy-ip 192.168.1.100 + +${C_BOLD}ENVIRONMENT VARIABLES:${C_NC} + WINDOWS_HOST_IP Windows host IP address + CLASH_PROXY_PORT Clash proxy port + ENABLE_PROXY Enable proxy (true/false) + GO_VERSION Go version to install + NODE_VERSION Node.js version to install + APT_MIRROR APT mirror name + +USAGE +} + +#=============================================================================== +# Argument Parsing +#=============================================================================== +DRY_RUN=false + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) print_usage; exit 0 ;; + -v|--version) echo "${SCRIPT_NAME} v${SCRIPT_VERSION}"; exit 0 ;; + --only) ONLY_MODULES="${2//,/ }"; shift 2 ;; + --skip) SKIP_MODULES="${2//,/ }"; shift 2 ;; + --no-proxy) ENABLE_PROXY="false"; shift ;; + --proxy-ip) WINDOWS_HOST_IP="$2"; shift 2 ;; + --proxy-port) CLASH_PROXY_PORT="$2"; shift 2 ;; + --mirror) APT_MIRROR="$2"; shift 2 ;; + --go-version) GO_VERSION="$2"; shift 2 ;; + --node-version) NODE_VERSION="$2"; shift 2 ;; + --font) NERD_FONT="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + *) + log_error "Unknown option: $1" + print_usage + exit 1 + ;; + esac + done +} + +#=============================================================================== +# Utility Functions +#=============================================================================== + +# Check if a command exists +check_cmd() { + command -v "$1" &>/dev/null +} + +# Run command as the installation user (not root) +run_as_user() { + local cmd="$1" + if [[ "$(id -u)" -eq 0 ]] && [[ "${INSTALL_USER}" != "root" ]]; then + su "${INSTALL_USER}" -s /bin/bash -c " +export HOME='${USER_HOME}' +export http_proxy='${http_proxy:-}' +export https_proxy='${https_proxy:-}' +export HTTP_PROXY='${HTTP_PROXY:-}' +export HTTPS_PROXY='${HTTPS_PROXY:-}' +export no_proxy='${no_proxy:-}' +export NO_PROXY='${NO_PROXY:-}' +export PATH='${PATH}' +cd '${USER_HOME}' +${cmd} +" + else + eval "${cmd}" + fi +} + +# Backup a file before modification +backup_file() { + local file="$1" + if [[ -f "$file" ]]; then + local backup="${file}.bak.$(date +%Y%m%d%H%M%S)" + cp "$file" "$backup" + log_step "Backed up: $(basename "$file") → $(basename "$backup")" + fi +} + +# Add a block to a shell config file (idempotent) +add_to_rc() { + local file="$1" + local marker="$2" + local content="$3" + + # Create file if it doesn't exist + if [[ ! -f "$file" ]]; then + touch "$file" + chown "${INSTALL_USER}:${INSTALL_USER}" "$file" 2>/dev/null || true + fi + + # Remove old block if exists, then add new + if grep -q "# >>> ${marker} >>>" "$file" 2>/dev/null; then + # Remove old block + sed -i "/# >>> ${marker} >>>/,/# <<< ${marker} <<>> ${marker} >>>" + echo "${content}" + echo "# <<< ${marker} <<<" + } >> "$file" +} + +# Compare versions: returns 0 if $1 >= $2 +version_gte() { + [[ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" == "$2" ]] +} + +# Ensure required commands are available +ensure_deps() { + local missing=() + local check_cmd_name + for dep in "$@"; do + check_cmd_name="$dep" + if [[ "$dep" == "fontconfig" ]]; then + check_cmd_name="fc-cache" + fi + if ! check_cmd "$check_cmd_name"; then + missing+=("$dep") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + log_step "Installing dependencies: ${missing[*]}" + if ! apt-get install -y -qq "${missing[@]}" >/dev/null 2>&1; then + log_error "Failed to install dependencies: ${missing[*]}" + return 1 + fi + + # Double check if dependencies were successfully installed + local still_missing=() + for dep in "${missing[@]}"; do + check_cmd_name="$dep" + if [[ "$dep" == "fontconfig" ]]; then + check_cmd_name="fc-cache" + fi + if ! check_cmd "$check_cmd_name"; then + still_missing+=("$dep") + fi + done + if [[ ${#still_missing[@]} -gt 0 ]]; then + log_error "Dependencies still missing after installation attempt: ${still_missing[*]}" + return 1 + fi + fi + return 0 +} + +# Download a GitHub release asset +download_github_asset() { + local repo="$1" + local pattern="$2" + local output="$3" + + ensure_deps curl jq || return 1 + + local api_url="https://api.github.com/repos/${repo}/releases/latest" + local url + url=$(curl -sL --connect-timeout 20 "${api_url}" \ + | jq -r --arg pat "${pattern}" '.assets[] | select(.name | test($pat; "i")) | .browser_download_url' \ + | head -1) + + if [[ -z "$url" || "$url" == "null" ]]; then + log_error "Failed to find GitHub asset: ${repo} matching '${pattern}'" + return 1 + fi + + log_step "Downloading: $(basename "$url")" + curl -fsSL --connect-timeout 30 --retry 3 -L "$url" -o "$output" +} + +# Install a .deb package from GitHub releases +install_github_deb() { + local repo="$1" + local pattern="$2" + local tmp_deb + tmp_deb=$(mktemp /tmp/wsl-setup-XXXXXX.deb) + + download_github_asset "$repo" "$pattern" "$tmp_deb" || return 1 + dpkg -i "$tmp_deb" >/dev/null 2>&1 || apt-get install -f -y -qq >/dev/null 2>&1 + rm -f "$tmp_deb" +} + +# Install a binary from a GitHub release tarball +install_github_tar_binary() { + local repo="$1" + local pattern="$2" + local binary_name="$3" + local tmp_dir + tmp_dir=$(mktemp -d /tmp/wsl-setup-XXXXXX) + local tmp_file="${tmp_dir}/download.tar.gz" + + download_github_asset "$repo" "$pattern" "$tmp_file" || { rm -rf "$tmp_dir"; return 1; } + tar -xzf "$tmp_file" -C "$tmp_dir" 2>/dev/null + local found + found=$(find "$tmp_dir" -name "$binary_name" -type f 2>/dev/null | head -1) + + if [[ -n "$found" ]]; then + install -m 755 "$found" "/usr/local/bin/${binary_name}" + else + log_error "Binary '${binary_name}' not found in archive" + rm -rf "$tmp_dir" + return 1 + fi + + rm -rf "$tmp_dir" +} + +# Check if a module should run +should_run_module() { + local module="$1" + if [[ -n "$ONLY_MODULES" ]]; then + [[ " $ONLY_MODULES " == *" $module "* ]] + else + [[ " $SKIP_MODULES " != *" $module "* ]] + fi +} + +#=============================================================================== +# System Detection +#=============================================================================== +detect_system() { + section_header "🖥️ System Detection" + + # --- OS Detection --- + if [[ ! -f /etc/os-release ]]; then + log_error "/etc/os-release not found — is this Ubuntu?" + exit 1 + fi + + # shellcheck source=/dev/null + source /etc/os-release + + UBUNTU_VERSION="${VERSION_ID:-unknown}" + UBUNTU_CODENAME="${VERSION_CODENAME:-unknown}" + + # Fallback codename detection + if [[ "$UBUNTU_CODENAME" == "unknown" ]]; then + UBUNTU_CODENAME=$(lsb_release -cs 2>/dev/null || echo "unknown") + fi + + case "$UBUNTU_VERSION" in + 20.04) UBUNTU_CODENAME="focal" ;; + 22.04) UBUNTU_CODENAME="jammy" ;; + 24.04) UBUNTU_CODENAME="noble" ;; + 26.04) : ;; # Use auto-detected codename + esac + + case "$UBUNTU_VERSION" in + 20.04|22.04|24.04|26.04) + log_success "Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — Supported ✓" + ;; + *) + log_warn "Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — Not officially tested" + log_warn "Will attempt best-effort compatibility" + ;; + esac + + # --- Architecture --- + ARCH=$(dpkg --print-architecture 2>/dev/null || echo "amd64") + case "$ARCH" in + amd64) GO_ARCH="amd64" ;; + arm64) GO_ARCH="arm64" ;; + *) log_error "Unsupported architecture: ${ARCH}"; exit 1 ;; + esac + log_info "Architecture: ${ARCH}" + + # --- WSL Detection --- + if grep -qi microsoft /proc/version 2>/dev/null; then + IS_WSL=true + log_info "Environment: WSL2" + + WIN_USER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r\n' || echo "") + if [[ -n "$WIN_USER" ]]; then + log_info "Windows user: ${WIN_USER}" + fi + else + IS_WSL=false + log_info "Environment: Native Linux" + fi + + # --- User --- + log_info "Install user: ${INSTALL_USER} (HOME=${USER_HOME})" +} + +#=============================================================================== +# Proxy Configuration +#=============================================================================== +setup_proxy() { + section_header "🌐 Proxy Configuration (Clash Verge)" + + local proxy_http="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" + local proxy_socks="socks5://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" + local no_proxy_list="localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12" + + log_info "HTTP proxy: ${proxy_http}" + log_info "SOCKS proxy: ${proxy_socks}" + + # 1. Unset any proxy environment variables first to test direct connectivity + unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY + + log_step "Testing direct connectivity (checking if TUN mode is handling traffic)..." + local direct_ok=false + if curl -sI --connect-timeout 5 https://github.com >/dev/null 2>&1 || curl -sI --connect-timeout 5 https://www.google.com >/dev/null 2>&1; then + direct_ok=true + log_success "Direct connection working (TUN mode or native routing active) ✓" + fi + + local proxy_ok=false + if [[ "$direct_ok" == "false" ]]; then + log_step "Direct connection unavailable. Testing manual proxy connectivity..." + export http_proxy="$proxy_http" + export https_proxy="$proxy_http" + export HTTP_PROXY="$proxy_http" + export HTTPS_PROXY="$proxy_http" + export all_proxy="$proxy_socks" + export ALL_PROXY="$proxy_socks" + export no_proxy="$no_proxy_list" + export NO_PROXY="$no_proxy_list" + + if curl -sI --connect-timeout 5 https://github.com >/dev/null 2>&1 || curl -sI --connect-timeout 5 https://www.google.com >/dev/null 2>&1; then + proxy_ok=true + log_success "Proxy connection working ✓" + else + log_warn "Proxy connectivity test failed — proxy port may be unreachable" + # Unset broken proxy variables so we don't break subsequent commands + unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY + fi + fi + + # --- Persist proxy_on/proxy_off to .bashrc and .profile --- + local proxy_shell_block + proxy_shell_block=$(cat << EOFPROXYBLOCK +proxy_on() { + export http_proxy="${proxy_http}" + export https_proxy="${proxy_http}" + export HTTP_PROXY="\${http_proxy}" + export HTTPS_PROXY="\${https_proxy}" + export all_proxy="${proxy_socks}" + export ALL_PROXY="\${all_proxy}" + export no_proxy="${no_proxy_list}" + export NO_PROXY="\${no_proxy}" + echo "🌐 Proxy ON: ${proxy_http}" +} + +proxy_off() { + unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY + echo "🚫 Proxy OFF" +} +EOFPROXYBLOCK +) + + if [[ "$proxy_ok" == "true" ]]; then + proxy_shell_block+=$'\n\n# Auto-enable proxy on shell start\nproxy_on >/dev/null 2>&1' + # Git proxy + run_as_user "git config --global http.proxy '${proxy_http}'" 2>/dev/null || true + run_as_user "git config --global https.proxy '${proxy_http}'" 2>/dev/null || true + AUTO_ENABLE_PROXY="true" + log_success "Proxy configured and enabled (auto-enable on shell start)" + else + proxy_shell_block+=$'\n\n# Proxy auto-enable skipped (tested offline or TUN mode active)' + # Remove git proxy since direct mode is used or proxy is unreachable + run_as_user "git config --global --unset http.proxy" 2>/dev/null || true + run_as_user "git config --global --unset https.proxy" 2>/dev/null || true + AUTO_ENABLE_PROXY="false" + if [[ "$direct_ok" == "true" ]]; then + log_success "Using transparent direct routing (helper functions written to shell rc)" + else + log_warn "Network seems offline — proxy configuration written but disabled" + fi + fi + + add_to_rc "${USER_HOME}/.bashrc" "WSL-PROXY" "$proxy_shell_block" + add_to_rc "${USER_HOME}/.profile" "WSL-PROXY" "$proxy_shell_block" +} + +#=============================================================================== +# APT Mirror Configuration +#=============================================================================== +setup_apt_mirror() { + section_header "📦 APT Mirror Configuration" + + local mirror_url + case "$APT_MIRROR" in + tuna) mirror_url="https://mirrors.tuna.tsinghua.edu.cn" ;; + ustc) mirror_url="https://mirrors.ustc.edu.cn" ;; + aliyun) mirror_url="https://mirrors.aliyun.com" ;; + default) log_info "Using default APT sources — skipping"; return 0 ;; + *) log_warn "Unknown mirror '${APT_MIRROR}', falling back to tuna" + mirror_url="https://mirrors.tuna.tsinghua.edu.cn" ;; + esac + + log_info "Mirror: ${mirror_url}" + log_info "Codename: ${UBUNTU_CODENAME}" + + if version_gte "$UBUNTU_VERSION" "24.04"; then + _setup_apt_deb822 "$mirror_url" + else + _setup_apt_traditional "$mirror_url" + fi + + log_step "Updating package lists..." + apt-get update -qq >/dev/null 2>&1 + log_success "APT mirror configured ✓" +} + +_setup_apt_traditional() { + local mirror_url="$1" + local sources_file="/etc/apt/sources.list" + + backup_file "$sources_file" + + cat > "$sources_file" << EOF +# Ubuntu APT Sources — Generated by wsl-dev-setup.sh +# Mirror: ${mirror_url} +# Date: $(date -Iseconds) + +deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME} main restricted universe multiverse +deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-updates main restricted universe multiverse +deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-backports main restricted universe multiverse +deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-security main restricted universe multiverse + +# deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME} main restricted universe multiverse +# deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-updates main restricted universe multiverse +# deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-backports main restricted universe multiverse +# deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-security main restricted universe multiverse +EOF + + log_step "Wrote traditional sources.list" +} + +_setup_apt_deb822() { + local mirror_url="$1" + local sources_file="/etc/apt/sources.list.d/ubuntu.sources" + + backup_file "$sources_file" + + # Disable legacy sources.list if it exists + [[ -f /etc/apt/sources.list ]] && \ + mv /etc/apt/sources.list /etc/apt/sources.list.disabled 2>/dev/null || true + + cat > "$sources_file" << EOF +## Ubuntu APT Sources (DEB822) — Generated by wsl-dev-setup.sh +## Mirror: ${mirror_url} +## Date: $(date -Iseconds) + +Types: deb +URIs: ${mirror_url}/ubuntu/ +Suites: ${UBUNTU_CODENAME} ${UBUNTU_CODENAME}-updates ${UBUNTU_CODENAME}-backports +Components: main restricted universe multiverse +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg + +Types: deb +URIs: ${mirror_url}/ubuntu/ +Suites: ${UBUNTU_CODENAME}-security +Components: main restricted universe multiverse +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg +EOF + + log_step "Wrote DEB822 format ubuntu.sources" +} + +#=============================================================================== +# Locale Configuration +#=============================================================================== +setup_locale() { + section_header "🌍 Locale & UTF-8 Configuration" + + if ! apt-get install -y -qq locales >/dev/null 2>&1; then + log_warn "Failed to install locales package. Skipping locale configuration." + return 0 + fi + + sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen 2>/dev/null || true + sed -i 's/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen 2>/dev/null || true + + if locale-gen en_US.UTF-8 zh_CN.UTF-8 >/dev/null 2>&1; then + update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LANGUAGE=en_US:en 2>/dev/null || true + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + log_success "Locale: en_US.UTF-8 + zh_CN.UTF-8 ✓" + else + log_warn "locale-gen failed to generate locales" + fi +} + +#=============================================================================== +# Base Tools Installation +#=============================================================================== +install_base_tools() { + section_header "🛠️ Base Tools Installation" + + # Common packages for all versions + local packages=( + # Core + curl wget git + # Network diagnostics + net-tools iputils-ping mtr-tiny dnsutils traceroute + # Build essentials + build-essential gcc g++ make cmake pkg-config + # Security / certificates + ca-certificates gnupg lsb-release + software-properties-common apt-transport-https + # Compression + unzip zip tar gzip bzip2 xz-utils + # JSON / text + jq tree + # System monitoring + htop ncdu + # SSH + openssh-client + # Terminal multiplexer + tmux + # Fonts + fontconfig + ) + + # telnet: package name varies by version + if apt-cache show inetutils-telnet >/dev/null 2>&1; then + packages+=(inetutils-telnet) + elif apt-cache show telnet >/dev/null 2>&1; then + packages+=(telnet) + fi + + # p7zip + if apt-cache show p7zip-full >/dev/null 2>&1; then + packages+=(p7zip-full) + fi + + log_info "Installing ${#packages[@]} packages..." + apt-get install -y -qq "${packages[@]}" >/dev/null 2>&1 + + # Verify key tools + local tools_ok=true + for tool in curl git wget ping mtr; do + if check_cmd "$tool"; then + log_step "${tool} ✓" + else + log_warn "${tool} — not found after install" + tools_ok=false + fi + done + + # Check telnet specifically (might be inetutils-telnet) + if check_cmd telnet; then + log_step "telnet ✓" + else + log_warn "telnet — not found (try: apt install telnet)" + fi + + if $tools_ok; then + log_success "Base tools installed ✓" + else + log_warn "Some tools may require manual installation" + fi +} + +#=============================================================================== +# Zsh + Zinit + Powerlevel10k +#=============================================================================== +install_zsh_terminal() { + section_header "💻 Zsh + Zinit + Powerlevel10k" + + # --- Install Zsh --- + if ! check_cmd zsh; then + log_step "Installing Zsh..." + if ! apt-get install -y -qq zsh >/dev/null 2>&1; then + log_error "Failed to install Zsh" + return 1 + fi + fi + + if ! check_cmd zsh; then + log_error "Zsh command not found after installation attempt" + return 1 + fi + log_success "Zsh $(zsh --version 2>/dev/null | head -c 20) ✓" + + # --- Install Zinit --- + local zinit_home="${USER_HOME}/.local/share/zinit/zinit.git" + if [[ ! -d "$zinit_home" ]]; then + log_step "Installing Zinit plugin manager..." + run_as_user "mkdir -p '${USER_HOME}/.local/share/zinit'" + if ! run_as_user "git clone --depth=1 https://github.com/zdharma-continuum/zinit.git '${zinit_home}'"; then + log_error "Failed to clone Zinit plugin manager from GitHub" + return 1 + fi + log_success "Zinit installed ✓" + else + log_info "Zinit already installed — pulling updates..." + run_as_user "cd '${zinit_home}' && git pull --quiet" || true + fi + + # --- Generate .zshrc --- + _generate_zshrc + + # --- Generate .p10k.zsh --- + _generate_p10k + + # --- Set Zsh as default shell --- + local zsh_path + zsh_path=$(which zsh) + if [[ -n "$zsh_path" ]]; then + local current_shell + current_shell=$(getent passwd "${INSTALL_USER}" 2>/dev/null | cut -d: -f7) + if [[ "$current_shell" != "$zsh_path" ]]; then + if chsh -s "$zsh_path" "$INSTALL_USER" 2>/dev/null; then + log_success "Default shell → Zsh ✓" + else + log_warn "Failed to set default shell to Zsh using chsh" + fi + else + log_info "Zsh already set as default shell" + fi + else + log_warn "Could not determine Zsh path — skipping shell change" + fi +} + +_generate_zshrc() { + local zshrc="${USER_HOME}/.zshrc" + backup_file "$zshrc" + + cat > "$zshrc" << 'EOFZSHRC' +# Ensure UTF-8 locale is fully active at the very start of the shell session +# (Important for CJK character width calculations in P10k and fzf-tab) +export LANG=en_US.UTF-8 +export LC_ALL=en_US.UTF-8 +export LC_CTYPE=en_US.UTF-8 + +# ╔══════════════════════════════════════════════════════════════╗ +# ║ Zsh Configuration ║ +# ║ Powered by Zinit + Powerlevel10k ║ +# ║ Generated by wsl-dev-setup.sh ║ +# ╚══════════════════════════════════════════════════════════════╝ + +# ┌──────────────────────────────────────────┐ +# │ Powerlevel10k Instant Prompt │ +# │ (must stay near the top of .zshrc) │ +# └──────────────────────────────────────────┘ +if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then + source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" +fi + +# ┌──────────────────────────────────────────┐ +# │ Zinit Initialization │ +# └──────────────────────────────────────────┘ +ZINIT_HOME="${HOME}/.local/share/zinit/zinit.git" +if [[ -f "${ZINIT_HOME}/zinit.zsh" ]]; then + source "${ZINIT_HOME}/zinit.zsh" + autoload -Uz _zinit + (( ${+_comps} )) && _comps[zinit]=_zinit +fi + +# ┌──────────────────────────────────────────┐ +# │ Completion System Setup │ +# └──────────────────────────────────────────┘ +# Initialize compinit synchronously so completion functions and plugins (like fzf-tab) +# hook into the completion system correctly and reliably. +mkdir -p "${HOME}/.cache" +autoload -Uz compinit +compinit -d "${HOME}/.cache/zcompdump" + +# ┌──────────────────────────────────────────┐ +# │ Theme: Powerlevel10k │ +# └──────────────────────────────────────────┘ +zinit ice depth=1 +zinit light romkatv/powerlevel10k + +# ┌──────────────────────────────────────────┐ +# │ Plugins (Zinit Turbo Mode) │ +# └──────────────────────────────────────────┘ +# Core plugins — loaded asynchronously for fast startup +zinit wait lucid for \ + atinit"ZINIT[COMPINIT_OPTS]=-C; zicompinit; zicdreplay" \ + zdharma-continuum/fast-syntax-highlighting \ + blockf \ + zsh-users/zsh-completions \ + atload"!_zsh_autosuggest_start" \ + zsh-users/zsh-autosuggestions + +# History substring search (↑↓ arrow key filtering) +# Loaded synchronously so widgets are registered before keybindings and syntax highlighting +zinit light zsh-users/zsh-history-substring-search + +# fzf-tab: replace default tab completion with fzf +zinit wait lucid for \ + Aloxaf/fzf-tab + +# Oh-My-Zsh snippets (cherry-picked essentials, NOT the whole framework) +zinit wait lucid for \ + OMZL::history.zsh \ + OMZL::key-bindings.zsh \ + OMZL::completion.zsh \ + OMZL::directories.zsh \ + OMZP::git \ + OMZP::sudo \ + OMZP::extract \ + OMZP::command-not-found + +# ┌──────────────────────────────────────────┐ +# │ History │ +# └──────────────────────────────────────────┘ +HISTFILE="${HOME}/.zsh_history" +HISTSIZE=50000 +SAVEHIST=50000 +setopt EXTENDED_HISTORY # Record timestamp +setopt HIST_EXPIRE_DUPS_FIRST # Expire duplicates first +setopt HIST_IGNORE_DUPS # Ignore contiguous duplicates +setopt HIST_IGNORE_ALL_DUPS # Remove older duplicates +setopt HIST_IGNORE_SPACE # Ignore commands starting with space +setopt HIST_FIND_NO_DUPS # No duplicates in search +setopt HIST_SAVE_NO_DUPS # No duplicates on save +setopt SHARE_HISTORY # Share history across sessions +setopt INC_APPEND_HISTORY # Append immediately + +# ┌──────────────────────────────────────────┐ +# │ Completion │ +# └──────────────────────────────────────────┘ +zstyle ':completion:*' matcher-list \ + 'm:{a-zA-Z}={A-Za-z}' \ + 'r:|[._-]=* r:|=*' +zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" +zstyle ':completion:*' menu no +zstyle ':completion:*:descriptions' format '[%d]' +zstyle ':completion:*' special-dirs true +zstyle ':fzf-tab:complete:cd:*' fzf-preview \ + 'eza -1 --color=always --icons $realpath 2>/dev/null || ls -1 --color=always $realpath 2>/dev/null' +zstyle ':fzf-tab:*' switch-group '<' '>' + +# ┌──────────────────────────────────────────┐ +# │ Key Bindings │ +# └──────────────────────────────────────────┘ +bindkey '^[[A' history-substring-search-up 2>/dev/null +bindkey '^[[B' history-substring-search-down 2>/dev/null +bindkey '^P' history-substring-search-up 2>/dev/null +bindkey '^N' history-substring-search-down 2>/dev/null +bindkey '^[OA' history-substring-search-up 2>/dev/null +bindkey '^[OB' history-substring-search-down 2>/dev/null + +# ┌──────────────────────────────────────────┐ +# │ Aliases │ +# └──────────────────────────────────────────┘ +# ── Modern tool replacements ── +if command -v eza &>/dev/null; then + alias ls='eza --icons --group-directories-first' + alias ll='eza -alh --icons --group-directories-first --git' + alias la='eza -a --icons --group-directories-first' + alias lt='eza --tree --icons --level=3' +else + alias ll='ls -alh --color=auto' + alias la='ls -A --color=auto' +fi + +if command -v bat &>/dev/null; then + alias cat='bat --paging=never --style=plain' + alias catn='bat --paging=never' +elif command -v batcat &>/dev/null; then + alias cat='batcat --paging=never --style=plain' + alias catn='batcat --paging=never' +fi + +command -v fdfind &>/dev/null && ! command -v fd &>/dev/null && alias fd='fdfind' + +# ── Navigation ── +alias ..='cd ..' +alias ...='cd ../..' +alias ....='cd ../../..' + +# ── Utilities ── +alias cls='clear' +alias ports='ss -tulnp' +alias myip='curl -s ifconfig.me && echo' +alias df='df -h' +alias free='free -h' +alias grep='grep --color=auto' +alias mkdir='mkdir -pv' +alias reload='exec zsh' + +# ── Git extras ── +alias glog='git log --oneline --graph --decorate -20' +alias gd='git diff' +alias gs='git status -sb' + +# ┌──────────────────────────────────────────┐ +# │ Environment — Go │ +# └──────────────────────────────────────────┘ +if [[ -d /usr/local/go ]]; then + export GOROOT=/usr/local/go + export GOPATH="${HOME}/go" + export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}" + export GOPROXY=https://goproxy.cn,direct + export GONOSUMDB="*" +fi + +# ┌──────────────────────────────────────────┐ +# │ Environment — fnm (Node.js) │ +# └──────────────────────────────────────────┘ +if [[ -d "${HOME}/.local/share/fnm" ]]; then + export FNM_DIR="${HOME}/.local/share/fnm" + export PATH="${FNM_DIR}:${PATH}" +fi +if command -v fnm &>/dev/null; then + eval "$(fnm env --use-on-cd --shell zsh)" +fi + +# ┌──────────────────────────────────────────┐ +# │ Tool Integrations │ +# └──────────────────────────────────────────┘ +# fzf +if [[ -f "${HOME}/.fzf.zsh" ]]; then + source "${HOME}/.fzf.zsh" +elif command -v fzf &>/dev/null; then + eval "$(fzf --zsh 2>/dev/null)" || true +fi + +# zoxide (smart cd) +if command -v zoxide &>/dev/null; then + eval "$(zoxide init zsh)" +fi + +# delta as git pager +if command -v delta &>/dev/null; then + export GIT_PAGER='delta' +fi + +# ┌──────────────────────────────────────────┐ +# │ Powerlevel10k Config │ +# └──────────────────────────────────────────┘ +# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh +[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh +EOFZSHRC + + # --- Append proxy config if enabled --- + if [[ "${ENABLE_PROXY}" == "true" ]]; then + cat >> "$zshrc" << EOFPROXY + +# >>> WSL-PROXY >>> +proxy_on() { + export http_proxy="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" + export https_proxy="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" + export HTTP_PROXY="\${http_proxy}" + export HTTPS_PROXY="\${https_proxy}" + export all_proxy="socks5://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" + export ALL_PROXY="\${all_proxy}" + export no_proxy="localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12" + export NO_PROXY="\${no_proxy}" + echo "🌐 Proxy ON: http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" +} + +proxy_off() { + unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY + echo "🚫 Proxy OFF" +} +EOFPROXY + + if [[ "${AUTO_ENABLE_PROXY:-false}" == "true" ]]; then + cat >> "$zshrc" << EOFPROXY_AUTO + +# Auto-enable proxy +proxy_on >/dev/null 2>&1 +# <<< WSL-PROXY <<< +EOFPROXY_AUTO + else + cat >> "$zshrc" << EOFPROXY_AUTO + +# Proxy auto-enable skipped (tested offline or TUN mode active) +# <<< WSL-PROXY <<< +EOFPROXY_AUTO + fi + fi + + chown "${INSTALL_USER}:${INSTALL_USER}" "$zshrc" + log_success ".zshrc generated ✓" +} + +_generate_p10k() { + local p10k_file="${USER_HOME}/.p10k.zsh" + + log_step "Generating Powerlevel10k config (Rainbow style)..." + + cat > "$p10k_file" << 'EOFP10K' +# ╔══════════════════════════════════════════════════════════════╗ +# ║ Powerlevel10k Configuration ║ +# ║ Style: Rainbow with Nerd Font v3 ║ +# ║ Generated by wsl-dev-setup.sh ║ +# ║ To reconfigure interactively: p10k configure ║ +# ╚══════════════════════════════════════════════════════════════╝ + +'builtin' 'local' '-a' 'p10k_config_opts' +[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') +[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') +[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand') +'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand' + +() { + emulate -L zsh -o extended_glob + + # Reset all P10k settings + unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR' + + # ══════════════════════════════════════ + # Prompt Layout + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( + os_icon # OS icon (Ubuntu/Linux) + dir # Current directory + vcs # Git status + newline # ── new line ── + prompt_char # ❯ or ✘ + ) + + typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( + status # Exit code on error + command_execution_time # Duration of last command + background_jobs # Background job indicator + direnv # direnv status + go_version # Go version (in Go projects) + node_version # Node.js version (in Node projects) + context # user@hostname (SSH only) + time # Current time HH:MM + ) + + # ══════════════════════════════════════ + # General + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_MODE=nerdfont-v3 + typeset -g POWERLEVEL9K_ICON_PADDING=moderate + typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT= + typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=true + + # Powerline separators + typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='\uE0B0' + typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='\uE0B2' + typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='\uE0B1' + typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='\uE0B3' + typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0' + typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2' + typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL='' + typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL='' + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + + # ══════════════════════════════════════ + # Prompt Char (❯) + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76 + typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196 + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='V' + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶' + typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= + + # ══════════════════════════════════════ + # OS Icon + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=255 + typeset -g POWERLEVEL9K_OS_ICON_BACKGROUND=24 + + # ══════════════════════════════════════ + # Directory + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_DIR_FOREGROUND=255 + typeset -g POWERLEVEL9K_DIR_BACKGROUND=31 + typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=153 + typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=255 + typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true + typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique + typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=3 + typeset -g POWERLEVEL9K_SHORTEN_DELIMITER= + typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80 + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40 + typeset -g POWERLEVEL9K_DIR_HYPERLINK=false + typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=v3 + typeset -g POWERLEVEL9K_DIR_NOT_WRITABLE_FOREGROUND=220 + typeset -g POWERLEVEL9K_DIR_NOT_WRITABLE_BACKGROUND=88 + typeset -g POWERLEVEL9K_DIR_CLASSES=() + + # ══════════════════════════════════════ + # VCS (Git) + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=0 + typeset -g POWERLEVEL9K_VCS_CLEAN_BACKGROUND=76 + typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=0 + typeset -g POWERLEVEL9K_VCS_MODIFIED_BACKGROUND=178 + typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=0 + typeset -g POWERLEVEL9K_VCS_UNTRACKED_BACKGROUND=166 + typeset -g POWERLEVEL9K_VCS_CONFLICTED_FOREGROUND=255 + typeset -g POWERLEVEL9K_VCS_CONFLICTED_BACKGROUND=196 + typeset -g POWERLEVEL9K_VCS_LOADING_FOREGROUND=244 + typeset -g POWERLEVEL9K_VCS_LOADING_BACKGROUND=240 + typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 ' + typeset -g POWERLEVEL9K_VCS_COMMIT_ICON='@' + typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1 + typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION= + typeset -g POWERLEVEL9K_VCS_BACKENDS=(git) + typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~' + + # ══════════════════════════════════════ + # Status (Exit Code) + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true + # OK — hidden by default (no noise) + typeset -g POWERLEVEL9K_STATUS_OK=false + typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=70 + typeset -g POWERLEVEL9K_STATUS_OK_BACKGROUND=238 + typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔' + # OK pipe + typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=70 + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_BACKGROUND=238 + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔' + # Error + typeset -g POWERLEVEL9K_STATUS_ERROR=true + typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=255 + typeset -g POWERLEVEL9K_STATUS_ERROR_BACKGROUND=124 + typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘' + # Error signal + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=255 + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_BACKGROUND=124 + typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘' + # Error pipe + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=255 + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_BACKGROUND=124 + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘' + + # ══════════════════════════════════════ + # Command Execution Time + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=248 + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_BACKGROUND=240 + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION= + + # ══════════════════════════════════════ + # Background Jobs + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=37 + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_BACKGROUND=238 + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false + + # ══════════════════════════════════════ + # Direnv + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=178 + typeset -g POWERLEVEL9K_DIRENV_BACKGROUND=238 + + # ══════════════════════════════════════ + # Go Version + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=255 + typeset -g POWERLEVEL9K_GO_VERSION_BACKGROUND=37 + typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true + typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='󰟓' + + # ══════════════════════════════════════ + # Node Version + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=255 + typeset -g POWERLEVEL9K_NODE_VERSION_BACKGROUND=70 + typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true + typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='󰎙' + + # ══════════════════════════════════════ + # Context (user@hostname) + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=255 + typeset -g POWERLEVEL9K_CONTEXT_BACKGROUND=240 + typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=255 + typeset -g POWERLEVEL9K_CONTEXT_ROOT_BACKGROUND=124 + typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT_EXPANSION,TEMPLATE}='%n@%m' + typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%B%n@%m' + # Only show in SSH sessions or as root + typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_CONTENT_EXPANSION= + typeset -g POWERLEVEL9K_ALWAYS_SHOW_CONTEXT=false + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_CONTENT_EXPANSION='%n@%m' + + # ══════════════════════════════════════ + # Time + # ══════════════════════════════════════ + typeset -g POWERLEVEL9K_TIME_FOREGROUND=255 + typeset -g POWERLEVEL9K_TIME_BACKGROUND=236 + typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M}' + typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false + typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION= + + # ══════════════════════════════════════ + # Transient Prompt + # ══════════════════════════════════════ + # Replace previous prompts with a minimal version to keep terminal clean + typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always + + # ══════════════════════════════════════ + # Instant Prompt + # ══════════════════════════════════════ + # Show prompt immediately while plugins load in background + typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose + + # ══════════════════════════════════════ + # Hot Reload + # ══════════════════════════════════════ + (( ! $+functions[p10k] )) || p10k reload +} + +(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]} +'builtin' 'unset' 'p10k_config_opts' +EOFP10K + + chown "${INSTALL_USER}:${INSTALL_USER}" "$p10k_file" + log_success ".p10k.zsh generated (Rainbow + Nerd Font v3) ✓" + log_info "To reconfigure interactively: ${C_BOLD}p10k configure${C_NC}" +} + +#=============================================================================== +# Nerd Font Installation +#=============================================================================== +install_nerd_fonts() { + section_header "🔤 Nerd Font: ${NERD_FONT}" + + ensure_deps curl jq unzip fontconfig || return 1 + + local font_dir="${USER_HOME}/.local/share/fonts/NerdFonts" + + # --- Check if already installed --- + if [[ -d "$font_dir" ]] && compgen -G "${font_dir}/*.ttf" >/dev/null 2>&1; then + local count + count=$(find "$font_dir" -name '*.ttf' 2>/dev/null | wc -l) + log_info "Nerd Fonts already installed (${count} files in ${font_dir})" + else + log_step "Downloading ${NERD_FONT} Nerd Font from GitHub..." + + local tmp_dir + tmp_dir=$(mktemp -d /tmp/nerd-font-XXXXXX) + + download_github_asset "ryanoasis/nerd-fonts" \ + "${NERD_FONT}\\.zip" \ + "${tmp_dir}/${NERD_FONT}.zip" || { + log_error "Failed to download Nerd Font" + rm -rf "$tmp_dir" + return 1 + } + + run_as_user "mkdir -p '${font_dir}'" + unzip -o -q "${tmp_dir}/${NERD_FONT}.zip" '*.ttf' -d "$font_dir" 2>/dev/null || \ + unzip -o -q "${tmp_dir}/${NERD_FONT}.zip" -d "$font_dir" 2>/dev/null + + # Clean up non-essential files + rm -f "${font_dir}"/*Windows*.ttf 2>/dev/null || true + rm -f "${font_dir}"/*.md "${font_dir}"/*.txt "${font_dir}"/*.json 2>/dev/null || true + + chown -R "${INSTALL_USER}:${INSTALL_USER}" "${font_dir}" + fc-cache -f "${font_dir}" >/dev/null 2>&1 + + rm -rf "$tmp_dir" + + local count + count=$(find "$font_dir" -name '*.ttf' 2>/dev/null | wc -l) + log_success "Installed ${count} font files → ${font_dir}" + fi + + # --- Copy to Windows side (for Windows Terminal) --- + if [[ "$IS_WSL" == "true" && -n "${WIN_USER}" ]]; then + local win_font_dir="/mnt/c/Users/${WIN_USER}/AppData/Local/Microsoft/Windows/Fonts" + if [[ -d "/mnt/c/Users/${WIN_USER}" ]]; then + mkdir -p "$win_font_dir" 2>/dev/null || true + if cp "${font_dir}"/*.ttf "$win_font_dir/" 2>/dev/null; then + log_success "Fonts copied to Windows → ${win_font_dir}" + else + log_warn "Could not copy fonts to Windows side (non-critical)" + fi + fi + fi + + echo "" + echo -e " ${C_YELLOW}╔══════════════════════════════════════════════════════╗${C_NC}" + echo -e " ${C_YELLOW}║${C_NC} ${C_BOLD}📌 Windows Terminal 字体配置:${C_NC} ${C_YELLOW}║${C_NC}" + echo -e " ${C_YELLOW}║${C_NC} ${C_YELLOW}║${C_NC}" + echo -e " ${C_YELLOW}║${C_NC} Settings → Profiles → Defaults → Appearance ${C_YELLOW}║${C_NC}" + echo -e " ${C_YELLOW}║${C_NC} → Font face: ${C_BOLD}JetBrainsMono Nerd Font${C_NC} ${C_YELLOW}║${C_NC}" + echo -e " ${C_YELLOW}║${C_NC} ${C_YELLOW}║${C_NC}" + echo -e " ${C_YELLOW}║${C_NC} 如字体未出现,请在 Windows 中手动安装: ${C_YELLOW}║${C_NC}" + echo -e " ${C_YELLOW}║${C_NC} 双击 .ttf 文件 → Install ${C_YELLOW}║${C_NC}" + echo -e " ${C_YELLOW}╚══════════════════════════════════════════════════════╝${C_NC}" + echo "" +} + +#=============================================================================== +# Modern CLI Tools +#=============================================================================== +install_modern_cli() { + section_header "⚡ Modern CLI Tools" + + _install_bat + _install_eza + _install_fd + _install_ripgrep + _install_fzf + _install_zoxide + _install_delta + _install_lazygit + + echo "" + log_success "Modern CLI tools installation complete ✓" +} + +# ── bat (better cat) ── +_install_bat() { + if check_cmd bat || check_cmd batcat; then + log_info "bat — already installed ✓" + return 0 + fi + + log_step "Installing bat..." + if version_gte "$UBUNTU_VERSION" "22.04"; then + apt-get install -y -qq bat >/dev/null 2>&1 + # Create symlink: batcat → bat + [[ -f /usr/bin/batcat ]] && ln -sf /usr/bin/batcat /usr/local/bin/bat 2>/dev/null || true + else + case "$ARCH" in + amd64) install_github_deb "sharkdp/bat" "bat_.*_amd64\\.deb" ;; + arm64) install_github_deb "sharkdp/bat" "bat_.*_arm64\\.deb" ;; + esac + fi + + if check_cmd bat || check_cmd batcat; then + log_success "bat installed ✓" + else + log_warn "bat — installation failed" + fi +} + +# ── eza (better ls) ── +_install_eza() { + if check_cmd eza; then + log_info "eza — already installed ✓" + return 0 + fi + + log_step "Installing eza..." + if version_gte "$UBUNTU_VERSION" "24.04"; then + apt-get install -y -qq eza >/dev/null 2>&1 + else + # Install from GitHub release + case "$ARCH" in + amd64) install_github_tar_binary "eza-community/eza" "eza_x86_64-unknown-linux-gnu\\.tar\\.gz" "eza" ;; + arm64) install_github_tar_binary "eza-community/eza" "eza_aarch64-unknown-linux-gnu\\.tar\\.gz" "eza" ;; + esac + fi + + if check_cmd eza; then + log_success "eza installed ✓" + else + log_warn "eza — installation failed" + fi +} + +# ── fd (better find) ── +_install_fd() { + if check_cmd fd || check_cmd fdfind; then + log_info "fd — already installed ✓" + return 0 + fi + + log_step "Installing fd-find..." + apt-get install -y -qq fd-find >/dev/null 2>&1 || { + case "$ARCH" in + amd64) install_github_deb "sharkdp/fd" "fd_.*_amd64\\.deb" ;; + arm64) install_github_deb "sharkdp/fd" "fd_.*_arm64\\.deb" ;; + esac + } + + # Create symlink: fdfind → fd + [[ -f /usr/bin/fdfind ]] && ln -sf /usr/bin/fdfind /usr/local/bin/fd 2>/dev/null || true + + if check_cmd fd || check_cmd fdfind; then + log_success "fd installed ✓" + else + log_warn "fd — installation failed" + fi +} + +# ── ripgrep (better grep) ── +_install_ripgrep() { + if check_cmd rg; then + log_info "ripgrep — already installed ✓" + return 0 + fi + + log_step "Installing ripgrep..." + apt-get install -y -qq ripgrep >/dev/null 2>&1 || { + case "$ARCH" in + amd64) install_github_deb "BurntSushi/ripgrep" "ripgrep_.*_amd64\\.deb" ;; + arm64) install_github_deb "BurntSushi/ripgrep" "ripgrep_.*_arm64\\.deb" ;; + esac + } + + if check_cmd rg; then + log_success "ripgrep installed ✓" + else + log_warn "ripgrep — installation failed" + fi +} + +# ── fzf (fuzzy finder) ── +_install_fzf() { + if check_cmd fzf; then + log_info "fzf — already installed ✓" + return 0 + fi + + log_step "Installing fzf..." + + # Try apt first for newer Ubuntu + if version_gte "$UBUNTU_VERSION" "22.04"; then + apt-get install -y -qq fzf >/dev/null 2>&1 + fi + + # Fallback to git installation (gets latest version + shell integrations) + if ! check_cmd fzf; then + run_as_user "git clone --depth 1 https://github.com/junegunn/fzf.git '${USER_HOME}/.fzf'" 2>/dev/null || true + if [[ -d "${USER_HOME}/.fzf" ]]; then + run_as_user "'${USER_HOME}/.fzf/install' --all --no-update-rc --no-bash --no-zsh" 2>/dev/null || true + # Add to path + ln -sf "${USER_HOME}/.fzf/bin/fzf" /usr/local/bin/fzf 2>/dev/null || true + fi + fi + + if check_cmd fzf; then + log_success "fzf installed ✓" + else + log_warn "fzf — installation failed" + fi +} + +# ── zoxide (better cd) ── +_install_zoxide() { + if check_cmd zoxide; then + log_info "zoxide — already installed ✓" + return 0 + fi + + log_step "Installing zoxide..." + if version_gte "$UBUNTU_VERSION" "24.04"; then + apt-get install -y -qq zoxide >/dev/null 2>&1 + else + case "$ARCH" in + amd64) install_github_deb "ajeetdsouza/zoxide" "zoxide_.*_amd64\\.deb" ;; + arm64) install_github_deb "ajeetdsouza/zoxide" "zoxide_.*_arm64\\.deb" ;; + esac + fi + + if check_cmd zoxide; then + log_success "zoxide installed ✓" + else + log_warn "zoxide — installation failed" + fi +} + +# ── delta (better diff / git pager) ── +_install_delta() { + if check_cmd delta; then + log_info "delta — already installed ✓" + return 0 + fi + + log_step "Installing git-delta..." + case "$ARCH" in + amd64) install_github_deb "dandavison/delta" "git-delta_.*_amd64\\.deb" ;; + arm64) install_github_deb "dandavison/delta" "git-delta_.*_arm64\\.deb" ;; + esac + + # Configure git to use delta + if check_cmd delta; then + run_as_user "git config --global core.pager delta" 2>/dev/null || true + run_as_user "git config --global interactive.diffFilter 'delta --color-only'" 2>/dev/null || true + run_as_user "git config --global delta.navigate true" 2>/dev/null || true + run_as_user "git config --global delta.light false" 2>/dev/null || true + run_as_user "git config --global delta.line-numbers true" 2>/dev/null || true + run_as_user "git config --global merge.conflictstyle diff3" 2>/dev/null || true + run_as_user "git config --global diff.colorMoved default" 2>/dev/null || true + log_success "delta installed + git configured ✓" + else + log_warn "delta — installation failed" + fi +} + +# ── lazygit (git TUI) ── +_install_lazygit() { + if check_cmd lazygit; then + log_info "lazygit — already installed ✓" + return 0 + fi + + log_step "Installing lazygit..." + case "$ARCH" in + amd64) install_github_tar_binary "jesseduffield/lazygit" "lazygit_.*_linux_x86_64\\.tar\\.gz" "lazygit" ;; + arm64) install_github_tar_binary "jesseduffield/lazygit" "lazygit_.*_linux_arm64\\.tar\\.gz" "lazygit" ;; + esac + + if check_cmd lazygit; then + log_success "lazygit installed ✓" + else + log_warn "lazygit — installation failed" + fi +} + +#=============================================================================== +# Go Installation +#=============================================================================== +install_golang() { + section_header "🐹 Go Installation" + + ensure_deps curl jq || return 1 + + # --- Resolve version --- + local target_version="$GO_VERSION" + if [[ "$target_version" == "latest" ]]; then + log_step "Detecting latest Go version..." + target_version=$(curl -sL --connect-timeout 15 'https://golang.google.cn/dl/?mode=json' \ + | jq -r '.[0].version' 2>/dev/null \ + | sed 's/^go//') + + if [[ -z "$target_version" || "$target_version" == "null" ]]; then + # Fallback to go.dev + target_version=$(curl -sL --connect-timeout 15 'https://go.dev/dl/?mode=json' \ + | jq -r '.[0].version' 2>/dev/null \ + | sed 's/^go//') + fi + + if [[ -z "$target_version" || "$target_version" == "null" ]]; then + log_error "Failed to detect latest Go version" + return 1 + fi + fi + + log_info "Target version: Go ${target_version}" + + # --- Check current installation --- + if check_cmd go; then + local current + current=$(go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "") + if [[ "$current" == "$target_version" ]]; then + log_success "Go ${target_version} already installed ✓" + _setup_go_env + return 0 + fi + log_info "Current Go: ${current} → Upgrading to ${target_version}" + fi + + # --- Download --- + local go_tarball="go${target_version}.linux-${GO_ARCH}.tar.gz" + local download_url="https://golang.google.cn/dl/${go_tarball}" + local tmp_file="/tmp/${go_tarball}" + + log_step "Downloading: ${go_tarball}" + if ! curl -fsSL --connect-timeout 30 --retry 3 "$download_url" -o "$tmp_file"; then + # Fallback to go.dev + download_url="https://go.dev/dl/${go_tarball}" + log_step "Retrying from go.dev..." + curl -fsSL --connect-timeout 30 --retry 3 "$download_url" -o "$tmp_file" || { + log_error "Failed to download Go" + return 1 + } + fi + + # --- Install --- + log_step "Installing to /usr/local/go..." + rm -rf /usr/local/go + tar -C /usr/local -xzf "$tmp_file" + rm -f "$tmp_file" + + # --- Create GOPATH --- + run_as_user "mkdir -p '${USER_HOME}/go/bin' '${USER_HOME}/go/src' '${USER_HOME}/go/pkg'" + + # --- Setup environment --- + _setup_go_env + + # --- Verify --- + local installed_version + installed_version=$(/usr/local/go/bin/go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "unknown") + log_success "Go ${installed_version} installed ✓" + log_info "GOROOT=/usr/local/go GOPATH=\${HOME}/go" + log_info "GOPROXY=https://goproxy.cn,direct" +} + +_setup_go_env() { + # System-wide (all users, all login shells) + cat > /etc/profile.d/golang.sh << 'EOFGO' +# Go environment — managed by wsl-dev-setup.sh +export GOROOT=/usr/local/go +export GOPATH="${HOME}/go" +export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}" +export GOPROXY=https://goproxy.cn,direct +export GONOSUMDB="*" +EOFGO + chmod 644 /etc/profile.d/golang.sh + + # .bashrc + local go_bashrc=' +if [[ -d /usr/local/go ]]; then + export GOROOT=/usr/local/go + export GOPATH="${HOME}/go" + export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}" + export GOPROXY=https://goproxy.cn,direct + export GONOSUMDB="*" +fi' + add_to_rc "${USER_HOME}/.bashrc" "GOLANG" "$go_bashrc" + + # .profile (for sh / login shells) + local go_profile=' +if [ -d /usr/local/go ]; then + export GOROOT=/usr/local/go + export GOPATH="${HOME}/go" + export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}" + export GOPROXY=https://goproxy.cn,direct + export GONOSUMDB="*" +fi' + add_to_rc "${USER_HOME}/.profile" "GOLANG" "$go_profile" + + # .zshrc already includes Go config via _generate_zshrc + log_step "Go env configured: /etc/profile.d, .bashrc, .profile, .zshrc" +} + +#=============================================================================== +# Node.js Installation (via fnm) +#=============================================================================== +install_nodejs() { + section_header "📗 Node.js LTS via fnm (Fast Node Manager)" + + ensure_deps curl jq unzip || return 1 + + local fnm_dir="${USER_HOME}/.local/share/fnm" + local fnm_bin="${fnm_dir}/fnm" + + # --- Install fnm --- + if [[ -x "$fnm_bin" ]]; then + log_info "fnm already installed ✓" + else + log_step "Installing fnm from GitHub..." + local tmp_dir + tmp_dir=$(mktemp -d /tmp/fnm-XXXXXX) + + local fnm_pattern + case "$ARCH" in + amd64) fnm_pattern="fnm-linux\\.zip" ;; + arm64) fnm_pattern="fnm-arm64\\.zip" ;; + esac + + download_github_asset "Schniz/fnm" "$fnm_pattern" "${tmp_dir}/fnm.zip" || { + log_error "Failed to download fnm" + rm -rf "$tmp_dir" + return 1 + } + + run_as_user "mkdir -p '${fnm_dir}'" + unzip -o -q "${tmp_dir}/fnm.zip" -d "$fnm_dir" + chmod +x "$fnm_bin" + chown -R "${INSTALL_USER}:${INSTALL_USER}" "$fnm_dir" + + # Also link to /usr/local/bin for root access + ln -sf "$fnm_bin" /usr/local/bin/fnm 2>/dev/null || true + + rm -rf "$tmp_dir" + log_success "fnm installed → ${fnm_dir}" + fi + + # --- Setup environment for fnm --- + _setup_fnm_env + + # --- Install Node.js LTS --- + log_step "Installing Node.js LTS..." + + # We need fnm in the PATH for the user, and specify shell bash to prevent "Can't infer shell!" warning + local install_cmd="export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; fnm install --lts" + if ! run_as_user "$install_cmd"; then + log_warn "First attempt to install Node.js LTS failed. Clearing partial downloads and retrying..." + run_as_user "rm -rf '${fnm_dir}/node-versions/.downloads'" 2>/dev/null || true + if ! run_as_user "$install_cmd"; then + log_error "Failed to install Node.js LTS" + return 1 + fi + fi + + run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; fnm default lts-latest" 2>/dev/null || true + + # --- Get installed version --- + local node_version + node_version=$(run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; node --version" 2>/dev/null || echo "unknown") + local npm_version + npm_version=$(run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm --version" 2>/dev/null || echo "unknown") + + log_success "Node.js ${node_version} (npm ${npm_version}) installed ✓" + + # --- Set npm China mirror --- + log_step "Setting npm registry → npmmirror.com" + run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm config set registry https://registry.npmmirror.com" 2>/dev/null || true + + # --- Install global npm tools --- + log_step "Installing tldr..." + run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm install -g tldr" 2>/dev/null || { + log_warn "tldr installation failed (non-critical)" + } + + log_success "Node.js environment configured ✓" + log_info "npm registry: https://registry.npmmirror.com" +} + +_setup_fnm_env() { + local fnm_dir="${USER_HOME}/.local/share/fnm" + + # .bashrc + local fnm_bashrc=" +if [[ -d \"${fnm_dir}\" ]]; then + export FNM_DIR=\"${fnm_dir}\" + export PATH=\"\${FNM_DIR}:\${PATH}\" + eval \"\$(fnm env --use-on-cd --shell bash)\" +fi" + add_to_rc "${USER_HOME}/.bashrc" "FNM-NODEJS" "$fnm_bashrc" + + # .profile + local fnm_profile=" +if [ -d \"${fnm_dir}\" ]; then + export FNM_DIR=\"${fnm_dir}\" + export PATH=\"\${FNM_DIR}:\${PATH}\" + eval \"\$(fnm env --use-on-cd)\" +fi" + add_to_rc "${USER_HOME}/.profile" "FNM-NODEJS" "$fnm_profile" + + # .zshrc already includes fnm config via _generate_zshrc + log_step "fnm env configured: .bashrc, .profile, .zshrc" +} + +#=============================================================================== +# Verification & Summary +#=============================================================================== +print_summary() { + local elapsed=$(( $(date +%s) - SCRIPT_START_TIME )) + local minutes=$(( elapsed / 60 )) + local seconds=$(( elapsed % 60 )) + + echo "" + echo -e "${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_NC}" + echo -e "${C_CYAN}║${C_NC} ${C_BOLD}📊 Installation Summary${C_NC} ${C_CYAN}║${C_NC}" + echo -e "${C_CYAN}╠══════════════════════════════════════════════════════════════╣${C_NC}" + + # System info + printf "${C_CYAN}║${C_NC} %-58s${C_CYAN}║${C_NC}\n" "🖥️ Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — ${ARCH}" + [[ "$IS_WSL" == "true" ]] && \ + printf "${C_CYAN}║${C_NC} %-58s${C_CYAN}║${C_NC}\n" "📦 WSL2 — Windows user: ${WIN_USER:-N/A}" + + echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}" + + # Module results + _check_result "APT Mirror" "[[ -n '${APT_MIRROR}' ]]" + _check_result "Locale" "locale 2>/dev/null | grep -q 'UTF-8'" + _check_result "curl" "check_cmd curl" + _check_result "git" "check_cmd git" + _check_result "wget" "check_cmd wget" + _check_result "ping" "check_cmd ping" + _check_result "mtr" "check_cmd mtr" + _check_result "Zsh" "check_cmd zsh" + _check_result "Zinit" "[[ -d '${USER_HOME}/.local/share/zinit/zinit.git' ]]" + _check_result "P10k Config" "[[ -f '${USER_HOME}/.p10k.zsh' ]]" + _check_result "Nerd Font" "compgen -G '${USER_HOME}/.local/share/fonts/NerdFonts/*.ttf' >/dev/null 2>&1" + _check_result "bat" "check_cmd bat || check_cmd batcat" + _check_result "eza" "check_cmd eza" + _check_result "fd" "check_cmd fd || check_cmd fdfind" + _check_result "ripgrep" "check_cmd rg" + _check_result "fzf" "check_cmd fzf" + _check_result "zoxide" "check_cmd zoxide" + _check_result "delta" "check_cmd delta" + _check_result "lazygit" "check_cmd lazygit" + _check_result "Go" "check_cmd go || [[ -x /usr/local/go/bin/go ]]" + _check_result "fnm" "[[ -x '${USER_HOME}/.local/share/fnm/fnm' ]]" + _check_result "Node.js" "run_as_user \"export PATH='${USER_HOME}/.local/share/fnm:\${PATH}'; eval \\\"\\\$(fnm env)\\\"; node --version\" >/dev/null 2>&1" + + echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}" + + # Versions + local go_ver="N/A" + [[ -x /usr/local/go/bin/go ]] && go_ver=$(/usr/local/go/bin/go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "N/A") + + local node_ver="N/A" + node_ver=$(run_as_user "export PATH='${USER_HOME}/.local/share/fnm:\${PATH}'; eval \"\$(fnm env)\" 2>/dev/null; node --version" 2>/dev/null || echo "N/A") + + printf "${C_CYAN}║${C_NC} %-58s${C_CYAN}║${C_NC}\n" "Go: ${go_ver} Node.js: ${node_ver}" + printf "${C_CYAN}║${C_NC} %-58s${C_CYAN}║${C_NC}\n" "⏱️ Completed in ${minutes}m ${seconds}s" + + if [[ -n "$FAILED_MODULES" ]]; then + echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}" + printf "${C_CYAN}║${C_NC} ${C_RED}⚠️ Failed modules: %-39s${C_NC}${C_CYAN}║${C_NC}\n" "$FAILED_MODULES" + fi + + echo -e "${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_NC}" + + # Post-install instructions + echo "" + echo -e "${C_BOLD}📌 Next Steps:${C_NC}" + echo -e " 1. Set Windows Terminal font: ${C_BOLD}JetBrainsMono Nerd Font${C_NC}" + echo -e " 2. Restart WSL: ${C_DIM}wsl --shutdown${C_NC} then reopen terminal" + echo -e " 3. (Optional) Reconfigure prompt: ${C_DIM}p10k configure${C_NC}" + echo -e " 4. (Optional) Toggle proxy: ${C_DIM}proxy_on${C_NC} / ${C_DIM}proxy_off${C_NC}" + echo "" +} + +_check_result() { + local label="$1" + local test_cmd="$2" + if eval "$test_cmd" 2>/dev/null; then + printf "${C_CYAN}║${C_NC} ${C_GREEN}✅${C_NC} %-55s${C_CYAN}║${C_NC}\n" "$label" + else + printf "${C_CYAN}║${C_NC} ${C_RED}❌${C_NC} %-55s${C_CYAN}║${C_NC}\n" "$label" + fi +} + +#=============================================================================== +# Main Entry Point +#=============================================================================== +main() { + parse_args "$@" + + # --- Root check --- + if [[ "$(id -u)" -ne 0 ]]; then + log_error "This script must be run as root." + echo "" + echo -e " Usage: ${C_BOLD}sudo bash ${SCRIPT_NAME}${C_NC}" + echo "" + exit 1 + fi + + # --- Banner --- + print_banner + + # --- Dry run --- + if [[ "$DRY_RUN" == "true" ]]; then + log_info "DRY RUN — showing what would be executed:" + for module in $ALL_MODULES; do + if should_run_module "$module"; then + echo -e " ${C_GREEN}▶${C_NC} $module" + else + echo -e " ${C_DIM}▷ $module (skipped)${C_NC}" + fi + done + exit 0 + fi + + # ── Phase 0: System Detection ── + detect_system + + # ── Phase 1: Network & Sources (needed for downloads) ── + if should_run_module "proxy" && [[ "$ENABLE_PROXY" == "true" ]]; then + setup_proxy || FAILED_MODULES="${FAILED_MODULES} proxy" + fi + + if should_run_module "mirror"; then + setup_apt_mirror || FAILED_MODULES="${FAILED_MODULES} mirror" + fi + + # ── Phase 2: System Foundation ── + if should_run_module "locale"; then + setup_locale || FAILED_MODULES="${FAILED_MODULES} locale" + fi + + if should_run_module "base-tools"; then + install_base_tools || FAILED_MODULES="${FAILED_MODULES} base-tools" + fi + + # ── Phase 3: Terminal Environment ── + if should_run_module "zsh"; then + install_zsh_terminal || FAILED_MODULES="${FAILED_MODULES} zsh" + fi + + if should_run_module "fonts"; then + install_nerd_fonts || FAILED_MODULES="${FAILED_MODULES} fonts" + fi + + if should_run_module "modern-cli"; then + install_modern_cli || FAILED_MODULES="${FAILED_MODULES} modern-cli" + fi + + # ── Phase 4: Development Languages ── + if should_run_module "golang"; then + install_golang || FAILED_MODULES="${FAILED_MODULES} golang" + fi + + if should_run_module "nodejs"; then + install_nodejs || FAILED_MODULES="${FAILED_MODULES} nodejs" + fi + + # ── Phase 5: Summary ── + print_summary +} + +# ── Execute ── +main "$@" diff --git a/35-黑苹果DELL/3-windows的WSL的性能.md b/35-黑苹果DELL/3-windows的WSL的性能.md deleted file mode 100644 index 69576d3..0000000 --- a/35-黑苹果DELL/3-windows的WSL的性能.md +++ /dev/null @@ -1,3 +0,0 @@ -windows的WSL2的系统,能否 原生的运行Codex和ClaudeCodeDesktop - -WSL2有无原生的桌面,windows可以直接访问操作的那种,类似于两套系统同时运行 \ No newline at end of file diff --git a/35-黑苹果DELL/3-windows的终端工具.md b/35-黑苹果DELL/3-windows的终端工具.md new file mode 100644 index 0000000..6687546 --- /dev/null +++ b/35-黑苹果DELL/3-windows的终端工具.md @@ -0,0 +1,15 @@ +我现在经常使用mobaxterm作为windows的终端,请你寻找更多好用的终端工具 + +需求如下: +1. 可以被破解 +2. 开源项目 需要活跃的开发 +3. 具有账号管理能力,能够管理很多的终端账号 +4. 具有现代化的设计及丰富的终端能力 +5. 寻找最新的mobaxterm的可用破解版 + +不可取 +1. 不要FinalShell +2. 不要SecurityCRT +3. 不要Xshell + +请你基于上述的需求,执行分析调研 给我可选的结果 \ No newline at end of file diff --git a/35-黑苹果DELL/4-windows协同MacOS/1-Codex使用分析-prompt.md b/35-黑苹果DELL/4-windows协同MacOS/1-Codex使用分析-prompt.md new file mode 100644 index 0000000..2d2da37 --- /dev/null +++ b/35-黑苹果DELL/4-windows协同MacOS/1-Codex使用分析-prompt.md @@ -0,0 +1,19 @@ +你是一名精通利用大模型Agent开发的工程师,你熟练的使用各类的Agent工具,如ClaudeCode Codex Reasonix OpenCode等 + +你更加偏向于使用桌面客户端的形式 + +你熟练使用windows和macos系统,你熟悉这两个系统的文件管理方式,文件权限管理方式,系统服务管理方式,网络配置管理方式 + +我有MacOS和Windows两台电脑,我的工作主力是Windows,我能否将MacOS设置为Agent执行的中枢,永不停机 + +现在的方案是 +1. 在windows上制作一个一键脚本,脚本需要通用,适合很多不同的项目 +2. 脚本的能力如下 + 1. windows将代码特定分支提交到Git仓库 + 2. windows通过ssh远程MacOS 执行命令行的拉取特定分支代码 + 3. Windows通过本机的Codex远程操作MacOS的Codex进行Agent运行 + 4. 任务完成之后,Windows获得通知 + 5. Windows通过脚本能力,SSH到远程MacOS,将代码特定分支提交到Git仓库 + 6. windows本地拉取特定分支代码,实现完整的开发流程 + +请你帮我分析上述方案是否合理,是否有更加合理的方案,如果有 请给出优化和改进的措施 diff --git a/35-黑苹果DELL/5-windows番茄工作法工具.md b/35-黑苹果DELL/5-windows番茄工作法工具.md new file mode 100644 index 0000000..7fbcd67 --- /dev/null +++ b/35-黑苹果DELL/5-windows番茄工作法工具.md @@ -0,0 +1,3 @@ +请你帮我分析windows下面能够支持番茄工作法的工具,能够强制开启休息 中断手头工作的那种 + +我现在使用的 \ No newline at end of file diff --git a/35-黑苹果DELL/6-微信数据备份.md b/35-黑苹果DELL/6-微信数据备份.md deleted file mode 100644 index c5db669..0000000 --- a/35-黑苹果DELL/6-微信数据备份.md +++ /dev/null @@ -1,80 +0,0 @@ -# 替换 desktop_user 和台式机 IP -ssh-copy-id -i C:\Users\wddsh\.ssh\id_ed25519.pub wdd@192.168.1.194 - -# 或手动追加(ssh-copy-id 若不可用) -cat C:\Users\wddsh\.ssh\id_ed25519.pub | ssh wdd@192.168.1.194 \ - "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" - -# 验证免密登录 -ssh -i C:\Users\wddsh\.ssh\id_ed25519 wdd@192.168.1.194 "echo SSH OK" - - -cat >> C:\Users\wddsh\.ssh\config << 'EOF' -Host wdd-pink-station - HostName 192.168.1.194 - User wdd - IdentityFile C:\Users\wddsh\.ssh\id_ed25519 - ServerAliveInterval 60 - ServerAliveCountMax 3 -EOF -chmod 600 ~/.ssh/config - -# 验证简写连接 -ssh wdd-pink-station "echo Connected" - - - -#!/usr/bin/env bash -set -euo pipefail - -# ── 配置区 ──────────────────────────────────────────────── -SRC="/c/Users/wddsh/Documents/xwechat_files" -REMOTE_HOST="wdd-pink-station" # 对应 ~/.ssh/config 中的 Host -REMOTE_DST="/c/Users/wdd/wechat_files/xwechat_files" -LOG_DIR="/c/Users/wddsh/wechat_backup/logs" -LOG_FILE="${LOG_DIR}/wechat_sync_$(date +%Y%m%d).log" -# ───────────────────────────────────────────────────────── - -mkdir -p "$LOG_DIR" - -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" -} - -log "======== 微信数据同步开始 ========" - -# ── 检测微信是否运行 ────────────────────────────────────── -if tasklist.exe 2>/dev/null | grep -qi "Weixin.exe"; then - log "警告:微信正在运行,跳过 db_storage(数据库锁定中)" - EXCLUDE_DB="--exclude=db_storage/" -else - log "微信未运行,全量同步数据库" - EXCLUDE_DB="" -fi - -# ── rsync 同步 ──────────────────────────────────────────── -rsync -avz \ - --progress \ - --partial \ - --delete \ - --exclude="temp/" \ - --exclude="cache/" \ - --exclude="apm_record/" \ - --exclude="*.lock" \ - ${EXCLUDE_DB} \ - -e "ssh -F ~/.ssh/config" \ - "${SRC}/" \ - "${REMOTE_HOST}:${REMOTE_DST}/" \ - 2>&1 | tee -a "$LOG_FILE" - -EXIT_CODE=${PIPESTATUS[0]} -if [ $EXIT_CODE -eq 0 ]; then - log "同步成功完成 ✓" -else - log "同步失败,rsync 退出码: $EXIT_CODE" -fi - -# ── 清理 30 天前日志 ────────────────────────────────────── -find "$LOG_DIR" -name "wechat_sync_*.log" -mtime +30 -delete 2>/dev/null - -log "======== 同步结束 ========" \ No newline at end of file diff --git a/35-黑苹果DELL/8-WSL2精通.md b/35-黑苹果DELL/8-WSL2精通.md deleted file mode 100644 index e69de29..0000000 diff --git a/35-黑苹果DELL/EFI-DELL-Latitude5400-15.7.7-可运行-WIFI无法使用.7z b/35-黑苹果DELL/EFI-DELL-Latitude5400-15.7.7-可运行-WIFI无法使用.7z new file mode 100644 index 0000000..22abd3d Binary files /dev/null and b/35-黑苹果DELL/EFI-DELL-Latitude5400-15.7.7-可运行-WIFI无法使用.7z differ diff --git a/99-项目模板/4-系统架构师/3-需求文档优化师.md b/99-项目模板/4-系统架构师/3-需求文档优化师.md new file mode 100644 index 0000000..ef4b60a --- /dev/null +++ b/99-项目模板/4-系统架构师/3-需求文档优化师.md @@ -0,0 +1,35 @@ +你是一名资深系统架构师,擅长设计简洁、可靠、可演进且避免过度设计的微服务系统。 + +设计原则: +1. 以第一性原理为基础,优先解决真实业务需求和核心工程问题。 +2. 不机械遵循设计文档;在充分理解约束的前提下,可提出更合理、更简洁、更可维护的替代方案。 +3. 充分发挥资深架构师经验,对系统进行必要优化,但避免炫技式设计和不必要的复杂化。 +4. 关注系统的可维护性、可扩展性、可观测性、稳定性、安全性和工程落地成本。 +5. 结合现代云原生架构实践,合理使用微服务、容器化、Kubernetes、Docker 等技术。 + +项目技术栈: +- 后端:Golang、Gin、GORM +- 数据库:PostgreSQL +- 缓存:Redis +- 前端:Vue 3、Vuetify 3、TypeScript +- 架构与基础设施:微服务架构、Docker、Kubernetes、云原生相关技术 + +禁止内容: +1. 禁止丢失原始的需求信息 +2. 禁止过度设计 +3. 禁止过度优化 +4. 禁止增加不切实际的复杂功能 +5. 禁止输出代码内容 +6. 禁止直接在原文档修改 +7. 禁止使用任何skill +8. 禁止读取非本次上下文的其他文件 + +遵守规范: +1. 应该先分析原始的需求文档,充分理解设计意图和原始的需求 +2. 需要说明优化点, 修改点, 新增点 +3. 不理解的地方, 关键设计点, 需要和用户确认 +4. 需要先设计然后充分确认之后, 再生成最终的定版的软件设计需求文档 +5. 生成文件应该创建新的文件,不要在原文档修改 + + +你的核心职责是基于用户原始需求设计文档,进行文档结构重塑、内容专业化和结构优化,补充不足,制定完善的定版的软件设计需求文档。请你基于附件生成标准规范的的原始需求文档 \ No newline at end of file diff --git a/99-项目模板/4-系统架构师/4-PRD转化大模型专家.md b/99-项目模板/4-系统架构师/4-PRD转化大模型专家.md new file mode 100644 index 0000000..b8c4428 --- /dev/null +++ b/99-项目模板/4-系统架构师/4-PRD转化大模型专家.md @@ -0,0 +1,40 @@ +你是一名资深系统架构师,擅长设计简洁、可靠、可演进且避免过度设计的微服务系统。 + +你的核心职责是根据规范的PRD充当系统架构师、研发项目经理的角色,将PRD拆解为其他大模型可以参考和依赖的系统简要设计和实现的大纲(简称大纲),此大纲会作为goal目标设定给其他的大模型Agent进行持久化任务 + +设计原则: +1. 以第一性原理为基础,优先解决真实业务需求和核心工程问题。 +2. 不机械遵循设计文档;在充分理解约束的前提下,可提出更合理、更简洁、更可维护的替代方案。 +3. 充分发挥资深架构师经验,对系统进行必要优化,但避免炫技式设计和不必要的复杂化。 +4. 关注系统的可维护性、可扩展性、可观测性、稳定性、安全性和工程落地成本。 +5. 结合现代云原生架构实践,合理使用微服务、容器化、Kubernetes、Docker 等技术。 + +项目技术栈: +- 后端:Golang、Gin、GORM +- 数据库:PostgreSQL +- 缓存:Redis +- 前端:Vue 3、Vuetify 3、TypeScript +- 架构与基础设施:微服务架构、Docker、Kubernetes、云原生相关技术 + +禁止内容 +1. 禁止丢失原始的需求信息 +2. 禁止过度设计 +3. 禁止直接在原文档修改 +4. 禁止使用任何skill +5. 禁止读取非本次上下文的其他文件 +6. 禁止输出代码规范和大面积的代码实现,由其他AgentSkill实现 +7. 禁止替其他Agent实现过多内容,需要把控核心需求和达成条件 + +核心原则 +1. 充分理解PRD的设计意图,设计层次递进的大纲文件 +2. 大纲文件应该是很多份的,分阶段,分文件输出多个简要设计实现大纲 +3. 大纲应该包含严格的阶段性开发基础条件要求,阶段性设计目标,需要包含严格的阶段性实现达成条件 +4. 大纲不应该包含具体的实现代码,除非是非常核心的设计实现 +5. 考虑实现可以并行,如果有可以并行实现的大纲,应该说明 +6. 不理解的地方, 关键设计点, 需要和用户确认 + +基本要求 +1. 应该建立新的目录,将所有的大纲文件统一输入 + + +请基于下面的文档,按照上述的要求拆分大纲 diff --git a/35-黑苹果DELL/7-linux终端工具.md b/99-项目模板/4-系统架构师/5-PRD转化AgentSkill大师.md similarity index 100% rename from 35-黑苹果DELL/7-linux终端工具.md rename to 99-项目模板/4-系统架构师/5-PRD转化AgentSkill大师.md