guanghulab/scripts/generate-daily-report.js

289 lines
8.8 KiB
JavaScript
Raw Permalink Normal View History

/**
* generate-daily-report.js
* 生成光湖开发日报内容并通过 GitHub GraphQL API 发布到 Discussions
*
* 使用方式
* GITHUB_TOKEN=xxx GITHUB_REPOSITORY=owner/repo node scripts/generate-daily-report.js
*
* 环境变量
* GITHUB_TOKEN - GitHub token (需要 discussions:write 权限)
* GITHUB_REPOSITORY - 仓库名 (owner/repo)
* DISCUSSION_CATEGORY - Discussions 分类名 (默认: Announcements)
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const { execSync } = require('child_process');
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY || '';
const DISCUSSION_CATEGORY = process.env.DISCUSSION_CATEGORY || 'Announcements';
const DEV_STATUS_PATH = path.join('.github', 'persona-brain', 'dev-status.json');
// ── GitHub GraphQL API 请求 ──
function graphqlRequest(query, variables = {}) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ query, variables });
const options = {
hostname: 'api.github.com',
path: '/graphql',
method: 'POST',
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'Content-Type': 'application/json',
'User-Agent': 'guanghulab-daily-report',
'Content-Length': Buffer.byteLength(data),
},
};
const req = https.request(options, (res) => {
let result = '';
res.on('data', (chunk) => result += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(result));
} catch {
reject(new Error(`GraphQL 解析失败: ${result.slice(0, 200)}`));
}
});
});
req.on('error', reject);
req.write(data);
req.end();
});
}
// ── 获取 Discussion 分类 ID ──
async function getCategoryId(owner, repo, categoryName) {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
discussionCategories(first: 20) {
nodes {
id
name
}
}
}
}
`;
const result = await graphqlRequest(query, { owner, repo });
const categories = result?.data?.repository?.discussionCategories?.nodes || [];
const category = categories.find(c => c.name === categoryName);
return category?.id || null;
}
// ── 获取仓库 ID ──
async function getRepositoryId(owner, repo) {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
id
}
}
`;
const result = await graphqlRequest(query, { owner, repo });
return result?.data?.repository?.id || null;
}
// ── 创建 Discussion ──
async function createDiscussion(repoId, categoryId, title, body) {
const mutation = `
mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
createDiscussion(input: {
repositoryId: $repoId,
categoryId: $categoryId,
title: $title,
body: $body
}) {
discussion {
id
url
}
}
}
`;
return graphqlRequest(mutation, { repoId, categoryId, title, body });
}
// ── 获取最近 24h 的 Git 活动 ──
function getRecentActivity() {
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
let commits = [];
let prActivity = '';
try {
const log = execSync(
`git log --since="${since}" --oneline --no-merges 2>/dev/null || true`,
{ encoding: 'utf8', timeout: 10000 }
).trim();
if (log) {
commits = log.split('\n').filter(Boolean).slice(0, 10);
}
} catch {
// Git log may fail in shallow clone
}
try {
const merges = execSync(
`git log --since="${since}" --oneline --merges 2>/dev/null || true`,
{ encoding: 'utf8', timeout: 10000 }
).trim();
if (merges) {
prActivity = merges.split('\n').filter(Boolean).slice(0, 5).join('\n');
}
} catch {
// Ignore errors
}
return { commits, prActivity };
}
// ── 生成日报 Markdown ──
function generateReport(devStatus, activity) {
const today = new Date();
const bjDate = new Date(today.getTime() + 8 * 60 * 60 * 1000);
const dateStr = bjDate.toISOString().split('T')[0];
const team = devStatus.team || [];
// 团队状态表格
let teamTable = '| 开发者 | 当前模块 | 连胜 | 状态 |\n| --- | --- | --- | --- |\n';
team.forEach(dev => {
const statusEmoji = {
'active': '🟢 活跃',
'waiting_syslog': '🟡 等待SYSLOG',
'waiting_broadcast': '🔵 等待广播',
'paused': '⏸️ 暂停',
};
const status = statusEmoji[dev.status] || dev.status;
const streak = dev.streak > 0 ? `🔥${dev.streak}` : '—';
teamTable += `| ${dev.name} | ${dev.module} | ${streak} | ${status} |\n`;
});
// 今日亮点
let highlights = '';
if (activity.commits.length > 0) {
highlights += activity.commits.slice(0, 5).map(c => `· ${c}`).join('\n') + '\n';
}
// 连胜记录
const topStreakers = team.filter(d => d.streak >= 5).sort((a, b) => b.streak - a.streak);
if (topStreakers.length > 0) {
highlights += topStreakers.map(d => `· 🔥 ${d.name} ${d.streak}连胜 — ${d.module}`).join('\n') + '\n';
}
if (!highlights) {
highlights = '· 团队持续推进中,期待新的突破!\n';
}
// 项目概览
const activeDevs = team.filter(d => d.status !== 'paused').length;
const summary = devStatus.summary || {};
const report = `# 🌊 光湖开发日报 · ${dateStr}
## 📊 团队状态
${teamTable}
## 🔥 今日亮点
${highlights}
## 🏗 项目概览
· 活跃开发者${activeDevs}
· 最高连胜${summary.top_streak || '无'}
· 代码仓库[guanghulab](https://github.com/qinfendebingshuo/guanghulab)
· 🏛 通感语言核系统编程语言 · 作品著作权认证中国版权保护中心
---
觉得有意思给个 Star 支持一下
💬 有问题或想聊聊欢迎到 [访客留言板](https://github.com/qinfendebingshuo/guanghulab/discussions/categories/访客留言板) 留言!
`;
return { title: `🌊 光湖开发日报 · ${dateStr}`, body: report };
}
// ── 主流程 ──
async function main() {
console.log('📰 开始生成光湖开发日报...');
// 读取 dev-status
let devStatus = {};
try {
devStatus = JSON.parse(fs.readFileSync(DEV_STATUS_PATH, 'utf8'));
} catch {
console.log('⚠️ dev-status.json 读取失败,使用空数据');
}
// 获取 Git 活动
const activity = getRecentActivity();
// 生成日报内容
const { title, body } = generateReport(devStatus, activity);
console.log(`📝 日报标题: ${title}`);
console.log('---');
console.log(body.slice(0, 500) + '...');
// 保存日报到本地(供调试和存档)
const reportDir = 'docs/daily-reports';
if (!fs.existsSync(reportDir)) {
fs.mkdirSync(reportDir, { recursive: true });
}
const bjDate = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().split('T')[0];
fs.writeFileSync(path.join(reportDir, `${bjDate}.md`), `# ${title}\n\n${body}`);
console.log(`✅ 日报已保存到 ${reportDir}/${bjDate}.md`);
// 发布到 Discussions
if (!GITHUB_TOKEN || !GITHUB_REPOSITORY) {
console.log('⚠️ GITHUB_TOKEN 或 GITHUB_REPOSITORY 未配置,跳过 Discussion 发布');
return;
}
const [owner, repo] = GITHUB_REPOSITORY.split('/');
if (!owner || !repo) {
console.log('⚠️ GITHUB_REPOSITORY 格式错误,应为 owner/repo');
return;
}
// 获取仓库和分类 ID
let repoId;
try {
repoId = await getRepositoryId(owner, repo);
} catch (err) {
console.log(`⚠️ 无法连接 GitHub API: ${err.message},跳过 Discussion 发布`);
return;
}
if (!repoId) {
console.log('⚠️ 无法获取仓库 ID跳过 Discussion 发布');
return;
}
let categoryId;
try {
categoryId = await getCategoryId(owner, repo, DISCUSSION_CATEGORY);
} catch (err) {
console.log(`⚠️ 获取分类失败: ${err.message},跳过 Discussion 发布`);
return;
}
if (!categoryId) {
console.log(`⚠️ 未找到 "${DISCUSSION_CATEGORY}" 分类,跳过 Discussion 发布`);
console.log(' 请先在仓库 Settings → Discussions 中创建该分类');
return;
}
// 创建 Discussion
try {
const result = await createDiscussion(repoId, categoryId, title, body);
if (result?.data?.createDiscussion?.discussion?.url) {
console.log(`✅ 日报已发布: ${result.data.createDiscussion.discussion.url}`);
} else {
console.log('⚠️ Discussion 创建结果:', JSON.stringify(result?.errors || result).slice(0, 500));
}
} catch (err) {
console.log(`⚠️ Discussion 发布失败: ${err.message}`);
}
}
main().catch(err => {
console.error('❌ 日报生成失败:', err.message);
process.exit(1);
});