Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions docs/migration-ubuntu-to-windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Ubuntu -> Windows 桌面应用迁移:备份与恢复方案

> 迁移 Clowder AI 数据从 Ubuntu 源端到 Windows 桌面应用的完整方案。
> 本文档 + `scripts/migrate-export.sh` + `scripts/restore.ps1` 构成可复用的迁移工具集(脚本已脱敏,无真实凭据)。

## 核心数据
| 数据 | 作用 | 源端位置 |
|------|------|----------|
| Redis `dump.rdb` | 会话历史 / 成员管理 / 帐号与密钥 / IM对接 | `~/.cat-cafe/redis-opensource/dump.rdb` |
| `.cat-cafe/` | 成员管理 / 帐号与密钥 / IM对接 / 治理注册表 | `<project>/.cat-cafe` |
| `*.sqlite` | 记忆 / 证据 / 事件 / 任务结果 | `<project>/evidence.sqlite` 等 |
| `data/transcripts/` | 会话记录 | `<project>/data/transcripts` |
| `~/.claude/` | Claude Code CLI skills/settings/hooks | `~/.claude` |

## 流程
Ubuntu 打包 + AES-256-CBC(PBKDF2) 加密 -> 推送私有 git 仓库 -> Windows `git pull` -> `restore.ps1` .NET 原生解密 -> 还原到桌面 app userData。

## 脚本
- `scripts/migrate-export.sh` — Ubuntu 侧:打包 + `openssl enc -aes-256-cbc -pbkdf2 -iter 100000` 加密 + 推送。**只读,不改原文件**。
- `scripts/restore.ps1` — Windows 侧:.NET 原生 `Rfc2898DeriveBytes`+`Aes` CBC 解密(**无 openssl 依赖,开箱即用**),还原到桌面 app userData(`%LOCALAPPDATA%\Clowder AI`)或 dev 源码树(`-Layout dev`)。

## Ubuntu 侧备份指令
```bash
# 1. 先改脚本里的 git 远程(GC_REMOTE 已脱敏为占位符,填你的私有仓库地址 + 凭据)
# GC_REMOTE="https://<user>:<token>@<your-git-host>/<owner>/cc-backup.git"
# 2. 跑备份(密码自己定,用于加密;还原时要用同一个密码)
cd /path/to/clowder-ai
./scripts/migrate-export.sh '你的加密密码'
# 产出:加密包 clowder-backup.tar.gz.enc + restore-manifest.json + restore.ps1,推送到 GC_REMOTE
```

`migrate-export.sh` 打包内容(staging 结构):
- `project/` — `.cat-cafe` / `.env` / `.mcp.json` / `data/` / `evidence|world|event-memory|task-outcome-episodes.sqlite`(+wal+shm)
- `home/.claude/` — `skills`(**`cp -rL` 解引用**,见已知问题 2)/ `settings.json` / `hooks`
- `redis/dump.rdb`

## Windows 侧恢复指令
```powershell
# 1. clone / pull 备份仓库
cd D:\cc-backup
git pull

# 2. 关掉 app + redis(防占 6399、防 --save 60 1 覆盖 dump.rdb)
Get-Process "*clowder*","redis-server" -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep 2

# 3. 跑还原(密码填备份时用的;desktop 布局自动检测)
.\restore.ps1 -BackupDir D:\cc-backup -Password '你的密码'

# 4. 等屏幕 [1/6]解密 -> [2/6]解包 -> [3/6].cat-cafe -> [4/6]sqlite+transcripts -> [5/6]Redis dump -> [6/6]后处理 全绿

# 5. 确认 service-manager.js 已是 --appendonly no(防 AOF 覆盖 dump.rdb 空库,见 #1169)
Select-String -Path "<desktop-dist>\resources\app\service-manager.js" -Pattern "appendonly"
# 应是 no。若是 yes:
# (Get-Content "<desktop-dist>\resources\app\service-manager.js") -replace '--appendonly yes','--appendonly no' | Set-Content "<desktop-dist>\resources\app\service-manager.js"

# 6. 启动 app(双击 Clowder AI.exe),等 20 秒
# 7. 验证:成员管理 / 帐号与密钥 / IM对接(飞书) / 历史对话 全在 = 成功
```

`restore.ps1` 还原映射(desktop 布局;路径源码确认自 `service-manager.js` 的 `resolveUserDataDir()` / `_buildApiEnv()` / `_ensureUserDataDir()`):
- `.cat-cafe` -> `project\.cat-cafe`(`findMonorepoRoot(cwd=project)` + `pnpm-workspace.yaml` marker)
- `evidence.sqlite` 等 -> userData 根(`EVIDENCE_DB` env)
- `transcripts` -> `data\transcripts`(`TRANSCRIPT_DATA_DIR`)
- `dump.rdb` -> `data\redis`(redis `--dir`),**并删除 `appendonlydir` 防 AOF 覆盖**
- `~/.claude` -> `%USERPROFILE%\.claude`
- desktop 布局跳过 `.env`/`.mcp.json`(桌面 app 走 `_buildApiEnv` 程序化构造,不读这两个文件)

## 已知问题
1. **Windows AOF 数据丢失 bug**([#1169](https://github.com/zts212653/clowder-ai/issues/1169) / PR [#1170](https://github.com/zts212653/clowder-ai/pull/1170)):桌面 app `service-manager.js` 在 Windows 启动 redis 带 `--appendonly yes`,bundled redis(cygwin/MSYS2 构建)的 AOF background rewrite `fork()` 不可靠会崩溃,空/损坏的 appendonly 文件在下次启动覆盖 `dump.rdb`,导致会话历史全部丢失。**恢复前必须确认 `--appendonly no`**(`restore.ps1` [5/6] 也会删 `appendonlydir` 兜底)。源端 Redis 7.0.15 写的 `dump.rdb` 可被 Windows 8.8.0 加载(实测 3678 keys,14 expired),版本兼容无碍。
2. **skills symlink 解包报错**(已修):旧版 `migrate-export.sh` 用 `cp -a` 保留 `~/.claude/skills` 的符号链接,Windows `tar.exe` 解不开 symlink 报 `Can't create ... Invalid argument`。已改 `cp -rL` 解引用复制实际文件。该报错无害(桌面 app 不依赖 Claude Code CLI skills),但已修复以消除噪声。

## 安全
- 加密密码由操作者控制,**不落脚本、不落日志**(仅作为 `migrate-export.sh` / `restore.ps1` 的参数传入)。
- `GC_REMOTE` 已脱敏为占位符,使用前填你自己的私有 git 仓库地址 + 凭据;不要把真实 token 提交进仓库。
- 备份包含 `.env` / `.mcp.json`(含密钥),仓库必须私有;不再需要时及时删除仓库并轮换其中暴露过的凭据。
130 changes: 130 additions & 0 deletions scripts/migrate-export.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/bin/bash
# Clowder AI 迁移打包脚本 (Ubuntu → GitCode → Windows)
# 用法: ./scripts/migrate-export.sh <密码>
# 产出: /tmp/cc-backup-repo/clowder-backup.tar.gz.enc + restore-manifest.json
# 可逆: 不修改任何原文件,纯只读导出
set -euo pipefail

PASSWORD="${1:-}"
if [ -z "$PASSWORD" ]; then
echo "用法: $0 <加密密码>"
echo "示例: $0 'my-secret-pass-2026'"
exit 1
fi

PROJECT_DIR="/home/developer/clowder-ai"
HOME_DIR="/home/developer"
REPO_DIR="/tmp/cc-backup-repo"
TARBALL="/tmp/clowder-backup.tar.gz"
ENC_FILE="$REPO_DIR/clowder-backup.tar.gz.enc"
GC_REMOTE="https://<user>:<token>@<your-git-host>/<owner>/cc-backup.git"

echo "=== [1/5] 准备打包目录 ==="
rm -rf "$REPO_DIR"
GIT_TERMINAL_PROMPT=0 git clone "${GC_REMOTE}" "$REPO_DIR" --quiet
echo " clone OK → $REPO_DIR"

echo "=== [2/5] 打包 (tar.gz) ==="
# 用 transform 保留原路径结构,但解包时能区分项目内 vs 家目录
# 结构:
# project/.cat-cafe/ project/.env project/.mcp.json
# project/data/transcripts/
# project/*.sqlite (+ -wal + -shm)
# home/.claude/skills home/.claude/settings.json home/.claude/hooks
# redis/dump.rdb
TAR_ROOT="/tmp/clowder-backup-staging"
rm -rf "$TAR_ROOT"
mkdir -p "$TAR_ROOT/project" "$TAR_ROOT/home/.claude" "$TAR_ROOT/redis"

# 项目内文件
cp -a "$PROJECT_DIR/.cat-cafe" "$TAR_ROOT/project/"
cp -a "$PROJECT_DIR/.env" "$TAR_ROOT/project/"
cp -a "$PROJECT_DIR/.mcp.json" "$TAR_ROOT/project/"
cp -a "$PROJECT_DIR/data" "$TAR_ROOT/project/"
# sqlite 主文件 + wal + shm (保证一致)
for db in evidence world event-memory task-outcome-episodes; do
cp -a "$PROJECT_DIR/$db.sqlite"* "$TAR_ROOT/project/"
done

# 家目录配置 (只迁 skills/settings/hooks,跳过 projects/telemetry 按Unix路径索引)
# skills 用 -rL 解引用: ~/.claude/skills 是指向 cat-cafe-skills/ 的 symlink,
# 保留链接会让 Windows tar.exe 解包时报 "Can't create ... Invalid argument" (解不开符号链接).
cp -rL "$HOME_DIR/.claude/skills" "$TAR_ROOT/home/.claude/"
cp -a "$HOME_DIR/.claude/settings.json" "$TAR_ROOT/home/.claude/"
cp -a "$HOME_DIR/.claude/hooks" "$TAR_ROOT/home/.claude/"

# Redis dump
cp -a "$HOME_DIR/.cat-cafe/redis-opensource/dump.rdb" "$TAR_ROOT/redis/"

echo " 打包内容:"
du -sh "$TAR_ROOT"/* 2>/dev/null | sed 's/^/ /'

tar czf "$TARBALL" -C "$TAR_ROOT" .
echo " tar.gz 大小: $(du -h "$TARBALL" | cut -f1)"

echo "=== [3/5] 加密 (AES-256-CBC + pbkdf2) ==="
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 \
-in "$TARBALL" \
-out "$ENC_FILE" \
-pass pass:"$PASSWORD"
echo " 加密包大小: $(du -h "$ENC_FILE" | cut -f1)"

echo "=== [4/5] 生成 restore-manifest.json (明文,不含密钥) ==="
cat > "$REPO_DIR/restore-manifest.json" <<'MANIFEST'
{
"version": 1,
"created_at": "PLACEHOLDER_TIMESTAMP",
"source_host": "ubuntu (/home/developer/clowder-ai)",
"encryption": "openssl AES-256-CBC + pbkdf2 iter=100000",
"decrypt_command": "openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 -in clowder-backup.tar.gz.enc -out clowder-backup.tar.gz -pass pass:<你的密码>",
"archive_structure": {
"project/": "放到 clowder-ai 项目根目录",
"home/.claude/": "放到用户家目录 ~/.claude/ (skills+settings+hooks only)",
"redis/dump.rdb": "放到 Windows Redis 数据目录: <project>/.cat-cafe/run/windows/data/dump.rdb"
},
"post_restore_actions": [
"1. 改 .mcp.json 绝对路径: /home/developer/clowder-ai → <新项目绝对路径>",
"2. 删除 .mcp.json 中 probe 条目的 cwd (指向旧 Downloads 路径)",
"3. 若拉新版源码,重新应用本地 patch (见 patch-list.txt)"
],
"skipped": [
"~/.claude/projects (Unix 路径索引,Windows 读不到)",
"~/.claude/telemetry (历史遥测)",
"practicecenter (独立 git 仓库,单独处理)",
"node_modules/.next/dist (新机重装生成)"
]
}
MANIFEST

# patch 清单
cat > "$REPO_DIR/patch-list.txt" <<'PATCHES'
# 本地 patch 清单 (若新机器拉新版源码需重新应用)
# 1. feishu/connector.yaml: FEISHU_GROUP_BOT_MENTIONS 字段 type: textarea → input
# 原因: ConfigFieldType 不支持 textarea,parser 会跳过整个 feishu manifest
# 2. connector-gateway-bootstrap.ts: prefixedEnv() 函数 (CATCAFE_ 前缀优先)
# 已在 src,新版本若已含则跳过
# 3. connector-hub pluginRegistry hotfix (dist 层): getter 懒加载 opts.pluginRegistry
# 已在 dist,新版本若已修复则跳过
# 4. /whoami 命令 (shared/dist/core-commands.js)
PATCHES

# 还原脚本自包含: clone cc-backup 后直接 .\restore.ps1 一键还原, 无需另找脚本
if [ -f "$PROJECT_DIR/scripts/restore.ps1" ]; then
cp -a "$PROJECT_DIR/scripts/restore.ps1" "$REPO_DIR/restore.ps1"
echo " restore.ps1 已打包进仓库 (desktop/dev 双布局, 一键还原)"
fi

echo "=== [5/5] 推送到备份仓库 ==="
cd "$REPO_DIR"
git add -A
git commit -m "backup: clowder-ai encrypted migration snapshot" --quiet
git push origin main --quiet 2>&1 | tail -3 || git push origin HEAD:main --quiet
echo
echo "✅ 完成!加密包已推到备份仓库 (地址见顶部 GC_REMOTE,此处不回显以防泄露凭据)"
echo " 文件: clowder-backup.tar.gz.enc ($(du -h "$ENC_FILE" | cut -f1))"
echo " 说明: restore-manifest.json + patch-list.txt (明文,无密钥)"
echo
echo "📌 Windows 还原: 见 restore-manifest.json + 运行 restore.ps1"

# 清理本地暂存
rm -rf "$TAR_ROOT" "$TARBALL"
Loading
Loading