211 lines
6.9 KiB
JavaScript
211 lines
6.9 KiB
JavaScript
/**
|
||
* 铸渊跨仓库同步脚本 · cross-repo-sync.js
|
||
*
|
||
* 将 guanghulab/persona-studio/ 下的文件同步到独立仓库 persona-studio
|
||
* 使用 GitHub API 进行文件级同步
|
||
*
|
||
* 需要环境变量:
|
||
* GITHUB_TOKEN 或 CROSS_REPO_TOKEN — 拥有 persona-studio 仓库写权限的 PAT
|
||
* SYNC_TARGET — 同步目标:all / brain / frontend / backend
|
||
*/
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const https = require('https');
|
||
|
||
const TOKEN = process.env.CROSS_REPO_TOKEN || '';
|
||
const TARGET_OWNER = 'qinfendebingshuo';
|
||
const TARGET_REPO = 'persona-studio';
|
||
const SYNC_TARGET = process.env.SYNC_TARGET || 'all';
|
||
|
||
const SOURCE_BASE = path.join(__dirname, '..', 'persona-studio');
|
||
|
||
// 同步映射:本仓库路径 → 目标仓库路径
|
||
const SYNC_MAP = {
|
||
brain: [
|
||
{ src: 'brain/persona-config.json', dest: 'brain/persona-config.json' },
|
||
{ src: 'brain/registry.json', dest: 'brain/registry.json' }
|
||
],
|
||
frontend: [
|
||
{ src: 'frontend/index.html', dest: 'frontend/index.html' },
|
||
{ src: 'frontend/chat.html', dest: 'frontend/chat.html' },
|
||
{ src: 'frontend/chat.js', dest: 'frontend/chat.js' },
|
||
{ src: 'frontend/style.css', dest: 'frontend/style.css' }
|
||
],
|
||
backend: [
|
||
{ src: 'backend/server.js', dest: 'backend/server.js' },
|
||
{ src: 'backend/routes/auth.js', dest: 'backend/routes/auth.js' },
|
||
{ src: 'backend/routes/chat.js', dest: 'backend/routes/chat.js' },
|
||
{ src: 'backend/routes/build.js', dest: 'backend/routes/build.js' },
|
||
{ src: 'backend/routes/notify.js', dest: 'backend/routes/notify.js' },
|
||
{ src: 'backend/brain/persona-engine.js', dest: 'backend/brain/persona-engine.js' },
|
||
{ src: 'backend/brain/model-router.js', dest: 'backend/brain/model-router.js' },
|
||
{ src: 'backend/brain/model-config.json', dest: 'backend/brain/model-config.json' },
|
||
{ src: 'backend/brain/memory-manager.js', dest: 'backend/brain/memory-manager.js' },
|
||
{ src: 'backend/brain/code-generator.js', dest: 'backend/brain/code-generator.js' },
|
||
{ src: 'backend/utils/email-sender.js', dest: 'backend/utils/email-sender.js' },
|
||
{ src: 'backend/utils/github-api.js', dest: 'backend/utils/github-api.js' }
|
||
]
|
||
};
|
||
|
||
/**
|
||
* GitHub API 请求
|
||
*/
|
||
function githubRequest(method, apiPath, body) {
|
||
return new Promise((resolve, reject) => {
|
||
const options = {
|
||
hostname: 'api.github.com',
|
||
path: apiPath,
|
||
method,
|
||
headers: {
|
||
'Authorization': `Bearer ${TOKEN}`,
|
||
'Accept': 'application/vnd.github+json',
|
||
'User-Agent': 'zhuyuan-cross-repo-sync',
|
||
'X-GitHub-Api-Version': '2022-11-28'
|
||
}
|
||
};
|
||
|
||
if (body) {
|
||
const bodyStr = JSON.stringify(body);
|
||
options.headers['Content-Type'] = 'application/json';
|
||
options.headers['Content-Length'] = Buffer.byteLength(bodyStr);
|
||
}
|
||
|
||
const req = https.request(options, (res) => {
|
||
let data = '';
|
||
res.on('data', (chunk) => { data += chunk; });
|
||
res.on('end', () => {
|
||
try {
|
||
resolve({ status: res.statusCode, data: data ? JSON.parse(data) : null });
|
||
} catch {
|
||
resolve({ status: res.statusCode, data: null });
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
req.setTimeout(15000, () => { req.destroy(); reject(new Error('GitHub API request timed out after 15s')); });
|
||
|
||
if (body) req.write(JSON.stringify(body));
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取目标仓库中文件的 SHA(用于更新)
|
||
*/
|
||
async function getFileSha(filePath) {
|
||
const apiPath = `/repos/${TARGET_OWNER}/${TARGET_REPO}/contents/${encodeURIComponent(filePath)}`;
|
||
const res = await githubRequest('GET', apiPath);
|
||
if (res.status === 200 && res.data && res.data.sha) {
|
||
return res.data.sha;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 同步单个文件
|
||
*/
|
||
async function syncFile(srcRelative, destPath) {
|
||
const srcFull = path.join(SOURCE_BASE, srcRelative);
|
||
|
||
if (!fs.existsSync(srcFull)) {
|
||
console.log(` ⏭️ 跳过(源文件不存在):${srcRelative}`);
|
||
return false;
|
||
}
|
||
|
||
const content = fs.readFileSync(srcFull);
|
||
const contentBase64 = content.toString('base64');
|
||
|
||
// 获取目标文件 SHA
|
||
const sha = await getFileSha(destPath);
|
||
|
||
const body = {
|
||
message: `🔄 铸渊同步 · ${destPath}`,
|
||
content: contentBase64,
|
||
committer: {
|
||
name: 'zhuyuan-sync',
|
||
email: 'zhuyuan-sync@users.noreply.github.com'
|
||
}
|
||
};
|
||
|
||
if (sha) {
|
||
body.sha = sha;
|
||
}
|
||
|
||
const apiPath = `/repos/${TARGET_OWNER}/${TARGET_REPO}/contents/${encodeURIComponent(destPath)}`;
|
||
const res = await githubRequest('PUT', apiPath, body);
|
||
|
||
if (res.status === 200 || res.status === 201) {
|
||
console.log(` ✅ 同步成功:${destPath}`);
|
||
return true;
|
||
} else {
|
||
console.log(` ❌ 同步失败 (${res.status}):${destPath} — ${res.data && res.data.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 主函数
|
||
*/
|
||
async function main() {
|
||
console.log('🔄 铸渊跨仓库同步启动');
|
||
console.log(` 目标仓库:${TARGET_OWNER}/${TARGET_REPO}`);
|
||
console.log(` 同步范围:${SYNC_TARGET}`);
|
||
console.log('');
|
||
|
||
if (!TOKEN) {
|
||
console.log('⚠️ 未设置 CROSS_REPO_TOKEN 或 GITHUB_TOKEN');
|
||
console.log(' 需要创建一个拥有 persona-studio 仓库写权限的 Personal Access Token');
|
||
console.log(' 然后在 guanghulab 仓库 Settings → Secrets 中添加为 CROSS_REPO_TOKEN');
|
||
console.log('');
|
||
console.log('📋 本次同步报告(仅检查,未推送):');
|
||
|
||
const targets = SYNC_TARGET === 'all' ? Object.keys(SYNC_MAP) : [SYNC_TARGET];
|
||
for (const target of targets) {
|
||
const files = SYNC_MAP[target] || [];
|
||
console.log(`\n 📂 ${target}:`);
|
||
for (const f of files) {
|
||
const srcFull = path.join(SOURCE_BASE, f.src);
|
||
const exists = fs.existsSync(srcFull);
|
||
console.log(` ${exists ? '✅' : '❌'} ${f.src} → ${f.dest}`);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 验证 token 有效性
|
||
const testRes = await githubRequest('GET', `/repos/${TARGET_OWNER}/${TARGET_REPO}`);
|
||
if (testRes.status !== 200) {
|
||
console.error(`❌ 无法访问目标仓库(${testRes.status})。请检查 Token 权限。`);
|
||
process.exit(1);
|
||
}
|
||
console.log(`✅ 目标仓库已验证:${testRes.data.full_name}\n`);
|
||
|
||
const targets = SYNC_TARGET === 'all' ? Object.keys(SYNC_MAP) : [SYNC_TARGET];
|
||
let synced = 0;
|
||
let failed = 0;
|
||
|
||
for (const target of targets) {
|
||
const files = SYNC_MAP[target] || [];
|
||
console.log(`📂 同步 ${target}(${files.length} 个文件):`);
|
||
|
||
for (const f of files) {
|
||
try {
|
||
const success = await syncFile(f.src, f.dest);
|
||
if (success) synced++; else failed++;
|
||
} catch (err) {
|
||
console.log(` ❌ 错误:${f.dest} — ${err.message}`);
|
||
failed++;
|
||
}
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
console.log(`\n🔄 同步完成:✅ ${synced} 成功 / ❌ ${failed} 失败`);
|
||
}
|
||
|
||
main().catch(err => {
|
||
console.error('跨仓库同步异常:', err.message);
|
||
process.exit(1);
|
||
});
|