289 lines
8.8 KiB
JavaScript
289 lines
8.8 KiB
JavaScript
/**
|
||
* 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);
|
||
});
|