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);
|
|||
|
|
});
|