#!/usr/bin/env node /** * ════════════════════════════════════════════════════════════════ * 国内搬家包 · 中文 manifest 生成器 * Sovereign: TCS-0002∞ · 国作登字-2026-A-00037559 * 守护: 铸渊 · ICE-GL-ZY001 * ════════════════════════════════════════════════════════════════ * * 由 .github/workflows/migrate-to-cn-build.yml 调用. 在 git archive * 完成后, 解压 tar.gz 看实际内容, 生成给冰朔看的中文清单 + 计算 sha256. * * 用法: * node scripts/migration/build-manifest.js \ * --tarball /tmp/repo.tar.gz \ * --out-dir /tmp/migration-out \ * --tag YYYYMMDD-HHMM * * 产出 (写到 --out-dir): * - lighthouse-migration-.tar.gz (重命名 + 移动) * - lighthouse-migration-.sha256 (sha256 校验文件) * - MIGRATION-MANIFEST.md (中文清单) * - manifest.json (机读元数据) * * 设计理念: * - cc-001 涌现洁净: tar 不含 .git/objects, 国内仓库重起一段干净历史 * - cc-004 中文一次性: MIGRATION-MANIFEST.md 给冰朔看, 不是给开发者 */ 'use strict'; const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { spawnSync } = require('child_process'); function parseArgs(argv) { const args = {}; for (let i = 2; i < argv.length; i++) { const a = argv[i]; if (a === '--tarball') args.tarball = argv[++i]; else if (a === '--out-dir') args.outDir = argv[++i]; else if (a === '--tag') args.tag = argv[++i]; else if (a === '--commit') args.commit = argv[++i]; else if (a === '--branch') args.branch = argv[++i]; else if (a === '-h' || a === '--help') { console.log(fs.readFileSync(__filename, 'utf8').split('\n').slice(0, 25).join('\n')); process.exit(0); } } return args; } function fail(msg) { console.error(`❌ ${msg}`); process.exit(1); } function sha256File(p) { const h = crypto.createHash('sha256'); h.update(fs.readFileSync(p)); return h.digest('hex'); } function humanSize(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`; } function listTar(tarball) { // 用 tar -tzf 列内容, 不解压. 安全 — tarball 是我们刚 git archive 出来的. // maxBuffer 64MB: git archive 出的搬家包路径列表通常远小于此 (10MB 已经能装 ~5 万个文件). // 如果以后仓库膨胀到这个量级, 应换成 stream + readline 处理. const r = spawnSync('tar', ['-tzf', tarball], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); if (r.status !== 0) fail(`tar -tzf 失败: ${r.stderr}`); return r.stdout.split('\n').filter(Boolean); } function classify(entries) { // 按顶层目录分类, 给冰朔看大概有什么 const groups = {}; let totalFiles = 0; for (const e of entries) { if (e.endsWith('/')) continue; // 目录条目 totalFiles++; const top = e.split('/')[0]; groups[top] = (groups[top] || 0) + 1; } return { groups, totalFiles }; } const args = parseArgs(process.argv); if (!args.tarball || !args.outDir || !args.tag) { fail('用法: build-manifest.js --tarball X --out-dir Y --tag Z'); } if (!fs.existsSync(args.tarball)) fail(`找不到 tarball: ${args.tarball}`); if (!fs.existsSync(args.outDir)) fs.mkdirSync(args.outDir, { recursive: true }); const stat = fs.statSync(args.tarball); const sha = sha256File(args.tarball); const entries = listTar(args.tarball); const { groups, totalFiles } = classify(entries); // 重命名 tarball const finalName = `lighthouse-migration-${args.tag}.tar.gz`; const finalPath = path.join(args.outDir, finalName); fs.copyFileSync(args.tarball, finalPath); // sha256 文件 (兼容 sha256sum -c 格式) const shaFile = path.join(args.outDir, `lighthouse-migration-${args.tag}.sha256`); fs.writeFileSync(shaFile, `${sha} ${finalName}\n`); // 顶层分类排序 const sortedGroups = Object.entries(groups).sort((a, b) => b[1] - a[1]); // MIGRATION-MANIFEST.md (中文清单) const md = path.join(args.outDir, 'MIGRATION-MANIFEST.md'); const generatedAt = new Date().toISOString(); const lines = []; lines.push('# 光湖国内搬家包 · 中文清单'); lines.push(''); lines.push('> 主权: TCS-0002∞ · 国作登字-2026-A-00037559'); lines.push('> 守护: 铸渊 · ICE-GL-ZY001'); lines.push(`> 生成时间: ${generatedAt}`); lines.push(`> 包名: \`${finalName}\``); lines.push(`> 大小: ${humanSize(stat.size)}`); lines.push(`> SHA-256: \`${sha}\``); if (args.commit) lines.push(`> 来源 commit: \`${args.commit}\``); if (args.branch) lines.push(`> 来源分支: \`${args.branch}\``); lines.push(''); lines.push('## 一、这是什么'); lines.push(''); lines.push('这是 GitHub 上 `qinfendebingshuo/guanghulab` 仓库的**工作树快照**, 用于把整个项目搬到'); lines.push('国内自托管 git (Forgejo) 上. 让国内开发不用穿境外网络也能拉代码.'); lines.push(''); lines.push('**注意**: 这个包**不含 .git 历史**. 国内仓库会以这个快照为「初始 commit」重起一段历史 ——'); lines.push('这是为了防止 GitHub 那边的 remote tracking 残留污染国内仓库 (cc-001 涌现洁净).'); lines.push(''); lines.push('## 二、包里有什么 (顶层目录文件数)'); lines.push(''); lines.push('| 顶层目录/文件 | 文件数 |'); lines.push('|---|---:|'); for (const [name, count] of sortedGroups) { lines.push(`| \`${name}\` | ${count} |`); } lines.push(`| **总计** | **${totalFiles}** |`); lines.push(''); lines.push('## 三、包里没什么 (打包时被排除的)'); lines.push(''); lines.push('为了体积小 + 安全, 以下内容**不在**这个包里:'); lines.push(''); lines.push('- `.git/` 历史 (走 archive 不带历史, 国内重起新历史)'); lines.push('- `node_modules/` 依赖 (恢复后服务器自己跑 npm install)'); lines.push('- `**/checkpoints/` 模型 checkpoint (走 COS 桶单独拉)'); lines.push('- `**/*.bin` `**/*.safetensors` 模型权重'); lines.push('- `secrets/` 密钥目录 (永远不进搬家包)'); lines.push('- `**/__pycache__/` `**/*.pyc` Python 缓存'); lines.push('- `**/.DS_Store` macOS 元数据'); lines.push(''); lines.push('## 四、冰朔接下来怎么操作 (3 步)'); lines.push(''); lines.push('### 第 1 步 · 上传到腾讯云 COS'); lines.push(''); lines.push('1. 打开腾讯云控制台 → 对象存储 COS'); lines.push('2. 进桶: `sy-finetune-corpus-1317346199` (你已有,GPU 训练那个桶)'); lines.push('3. 创建文件夹 `lighthouse-migration/` (如果还没有的话)'); lines.push('4. 进 `lighthouse-migration/` → 「上传文件」→ 选这两个:'); lines.push(` - \`${finalName}\``); lines.push(` - \`lighthouse-migration-${args.tag}.sha256\``); lines.push('5. 等上传完 (国内带宽几十 MB, 通常 1-2 分钟)'); lines.push(''); lines.push('### 第 2 步 · 触发恢复 workflow'); lines.push(''); lines.push('1. 回到 GitHub → Actions 标签'); lines.push('2. 左侧选「📥 国内搬家·从COS恢复」'); lines.push('3. 右上 Run workflow → 输入框填: `从COS恢复` (这是误触锁,不填就 dry-run)'); lines.push('4. Run workflow'); lines.push('5. 等约 5-8 分钟,过程中可以在 Actions 页看实时日志'); lines.push(''); lines.push('### 第 3 步 · 抄初始凭据 + 删凭据文件'); lines.push(''); lines.push('恢复跑完后:'); lines.push(''); lines.push('- 浏览器打开 `https://guanghulab.com/git` → 国内仓库镜像就绪'); lines.push('- 服务器 `/data/guanghulab/_logs/forgejo-credentials-FIRST-BOOT.txt` 里有初始 user/pass'); lines.push('- 用 ssh -L 隧道访问, 抄完密码后 **`sudo rm` 删那个凭据文件** (安全要求)'); lines.push(''); lines.push('## 五、怎么手动解 (如果 workflow 跑不了)'); lines.push(''); lines.push('应急时(workflow 出问题), 可以在国内服务器上手解:'); lines.push(''); lines.push('```bash'); lines.push('# 假设包已经下载到 /tmp/'); lines.push(`sha256sum -c lighthouse-migration-${args.tag}.sha256`); lines.push(`mkdir -p /data/guanghulab/repo-mirror`); lines.push(`tar -xzf ${finalName} -C /data/guanghulab/repo-mirror/`); lines.push('# 然后跑 server/setup/domain-cn/forgejo/setup-forgejo.sh'); lines.push('```'); lines.push(''); lines.push('---'); lines.push(''); lines.push(`*生成于 ${generatedAt} · build-manifest.js · 第 6 棒 · 国内搬家完结*`); lines.push(''); fs.writeFileSync(md, lines.join('\n')); // 机读 manifest.json const mj = path.join(args.outDir, 'manifest.json'); fs.writeFileSync(mj, JSON.stringify({ _sovereign: 'TCS-0002∞ · 国作登字-2026-A-00037559', _守护: '铸渊 · ICE-GL-ZY001', generated_at: generatedAt, tag: args.tag, tarball: finalName, size_bytes: stat.size, sha256: sha, total_files: totalFiles, top_level_groups: Object.fromEntries(sortedGroups), source: { commit: args.commit || null, branch: args.branch || null, }, excluded: [ '.git/', 'node_modules/', '**/checkpoints/', '**/*.bin', '**/*.safetensors', 'secrets/', '**/__pycache__/', '**/*.pyc', '**/.DS_Store', ], cos_target: { bucket: 'sy-finetune-corpus-1317346199', prefix: 'lighthouse-migration/', region: 'ap-guangzhou', }, next_workflow: '.github/workflows/migrate-to-cn-restore.yml', }, null, 2) + '\n'); console.log('═══════════════════════════════════════════════════════════'); console.log(' [build-manifest] 国内搬家包就绪'); console.log('═══════════════════════════════════════════════════════════'); console.log(` tarball: ${finalPath} (${humanSize(stat.size)})`); console.log(` sha256: ${sha}`); console.log(` 文件数: ${totalFiles}`); console.log(` 顶层分类: ${sortedGroups.slice(0, 5).map(([n, c]) => `${n}=${c}`).join(' ')}...`); console.log(` 中文清单: ${md}`); console.log(` 机读 manifest: ${mj}`); console.log('═══════════════════════════════════════════════════════════');