diff --git a/.github/brain/bulletin-board-today.json b/.github/brain/bulletin-board-today.json new file mode 100644 index 00000000..1a5e759d --- /dev/null +++ b/.github/brain/bulletin-board-today.json @@ -0,0 +1 @@ +{"date":"","records":[]} diff --git a/.github/workflows/daily-maintenance.yml b/.github/workflows/daily-maintenance.yml index 59d97f1b..4272cba3 100644 --- a/.github/workflows/daily-maintenance.yml +++ b/.github/workflows/daily-maintenance.yml @@ -22,6 +22,24 @@ jobs: with: node-version: '20' + # ── Step 0: 清空公告栏缓存(每日重置) ── + - name: 清空公告栏缓存 + run: | + CACHE_FILE=".github/brain/bulletin-board-today.json" + TODAY=$(TZ="Asia/Shanghai" date +%Y-%m-%d) + if [ -f "$CACHE_FILE" ]; then + CACHE_DATE=$(jq -r '.date // ""' "$CACHE_FILE" 2>/dev/null || echo "") + if [ "$CACHE_DATE" != "$TODAY" ]; then + echo "{\"date\":\"$TODAY\",\"records\":[]}" > "$CACHE_FILE" + echo "✅ 公告栏缓存已清空(旧日期: $CACHE_DATE)" + else + echo "📌 公告栏缓存日期匹配今天 ($TODAY),无需清空" + fi + else + echo "{\"date\":\"$TODAY\",\"records\":[]}" > "$CACHE_FILE" + echo "✅ 创建新的公告栏缓存文件" + fi + # ── Step 1: 检查 brain/ 目录完整性 ── - name: 检查 brain/ 目录完整性 id: brain_check @@ -96,7 +114,7 @@ jobs: run: | git config user.name "铸渊 Maintenance Agent" git config user.email "actions@guanghulab.com" - git add brain/system-health.json + git add brain/system-health.json .github/brain/bulletin-board-today.json if git diff --cached --quiet; then echo "📌 无变更需要提交" else diff --git a/.github/workflows/update-readme-bulletin.yml b/.github/workflows/update-readme-bulletin.yml index 6247d891..1571d635 100644 --- a/.github/workflows/update-readme-bulletin.yml +++ b/.github/workflows/update-readme-bulletin.yml @@ -77,7 +77,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add README.md + git add README.md .github/brain/bulletin-board-today.json if git diff --cached --quiet; then echo "📢 公告区无变化,跳过提交" else diff --git a/scripts/update-readme-bulletin.js b/scripts/update-readme-bulletin.js index a295e6ac..5dbcdd6d 100644 --- a/scripts/update-readme-bulletin.js +++ b/scripts/update-readme-bulletin.js @@ -30,6 +30,7 @@ const MEMORY_PATH = path.join(__dirname, '..', '.github', 'brain', 'memory.json' const PERSONA_MEMORY_PATH = path.join(__dirname, '..', '.github', 'persona-brain', 'memory.json'); const DEV_STATUS_PATH = path.join(__dirname, '..', '.github', 'persona-brain', 'dev-status.json'); const COLLABORATORS_PATH = path.join(__dirname, '..', '.github', 'brain', 'collaborators.json'); +const BULLETIN_CACHE_PATH = path.join(__dirname, '..', '.github', 'brain', 'bulletin-board-today.json'); const MAX_BINGSHUO_ENTRIES = 15; const MAX_COLLAB_ENTRIES = 20; @@ -101,6 +102,21 @@ function formatTime(ts) { return `${get('month')}-${get('day')} ${get('hour')}:${get('minute')}`; } +function formatTimeShort(ts) { + if (!ts) return '—'; + const d = new Date(ts); + if (isNaN(d.getTime())) return '—'; + const fmt = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + const parts = fmt.formatToParts(d); + const get = (type) => (parts.find(p => p.type === type) || {}).value || ''; + return `${get('hour')}:${get('minute')}`; +} + function todayDateStr() { const fmt = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', @@ -149,6 +165,54 @@ function loadMemoryEvents() { return events; } +/* ── 公告栏缓存(当日追加式) ─────────────────────────── */ + +function loadBulletinCache() { + const today = todayDateStr(); + try { + if (fs.existsSync(BULLETIN_CACHE_PATH)) { + const cache = JSON.parse(fs.readFileSync(BULLETIN_CACHE_PATH, 'utf8')); + if (cache.date === today) { + return cache; + } + } + } catch (err) { + console.log(`⚠️ 公告栏缓存读取失败: ${err.message}`); + } + return { date: today, records: [] }; +} + +function saveBulletinCache(cache) { + try { + fs.writeFileSync(BULLETIN_CACHE_PATH, JSON.stringify(cache, null, 2), 'utf8'); + } catch (err) { + console.log(`⚠️ 公告栏缓存写入失败: ${err.message}`); + } +} + +function appendToCache(cache, newEntries) { + const existingKeys = new Set(cache.records.map(r => r.key)); + let added = 0; + for (const e of newEntries) { + const key = `${e.actor}|${e.module || '—'}|${e.ts || ''}`; + if (!existingKeys.has(key)) { + existingKeys.add(key); + cache.records.push({ + key, + ts: e.ts, + actor: e.actor, + module: e.module || '—', + result: e.result, + icon: e.icon, + sortKey: e.sortKey, + }); + added++; + } + } + cache.records.sort((a, b) => a.sortKey - b.sortKey); + return added; +} + /* ── 分类事件为冰朔/合作者 ─────────────────────────── */ function classifyEvents(events) { @@ -474,26 +538,19 @@ function buildBingshuoAlert(issues) { return alert; } -/* ── 生成合作者公告表格 ─────────────────────────── */ +/* ── 生成合作者公告表格(追加式,从缓存构建) ─────────────────────────── */ -function buildCollabBulletin(entries) { - const sorted = [...entries].sort((a, b) => b.sortKey - a.sortKey); - const seen = new Set(); - const unique = []; - for (const e of sorted) { - const key = `${e.actor}|${e.module}|${formatTime(e.ts)}`; - if (!seen.has(key)) { seen.add(key); unique.push(e); } - } - - const display = unique.slice(0, MAX_COLLAB_ENTRIES); +function buildCollabBulletin(cacheRecords) { + const today = todayDateStr(); + const display = cacheRecords.slice(0, MAX_COLLAB_ENTRIES); if (display.length === 0) { - return '| 时间 | 合作者 | 模块 | 状态 |\n|------|--------|------|------|\n| 🕐 暂无记录 | — | — | 等待模块推送 |'; + return `👥 合作者公告栏(${today})\n\n| 时间 | 合作者 | 模块 | 状态 |\n|------|--------|------|------|\n| 🕐 暂无记录 | — | — | 等待模块推送 |`; } const rows = display.map(e => - `| ${formatTime(e.ts)} | ${e.actor} | \`${e.module || '—'}/\` | ${e.icon} ${e.result === 'success' || e.result === 'passed' ? '上传成功' : e.result === 'failed' || e.result === 'failure' ? '❌ 上传失败' : '已更新'} |` + `| ${formatTimeShort(e.ts)} | ${e.actor} | \`${e.module || '—'}/\` | ${e.icon} ${e.result === 'success' || e.result === 'passed' ? '上传成功' : e.result === 'failed' || e.result === 'failure' ? '❌ 上传失败' : '已更新'} |` ); - return `| 时间 | 合作者 | 模块 | 状态 |\n|------|--------|------|------|\n${rows.join('\n')}`; + return `👥 合作者公告栏(${today})\n\n| 时间 | 合作者 | 模块 | 状态 |\n|------|--------|------|------|\n${rows.join('\n')}`; } /* ── 生成合作者提醒 ─────────────────────────── */ @@ -687,18 +744,24 @@ async function main() { console.log(`\n📊 冰朔合计: ${allBingshuo.length} 条`); console.log(`📊 合作者合计: ${allCollab.length} 条\n`); - // 5. 检测需要干预的问题 + // 5. 合作者公告栏:追加式缓存 + const cache = loadBulletinCache(); + const addedCount = appendToCache(cache, allCollab); + saveBulletinCache(cache); + console.log(`📋 公告栏缓存: ${cache.records.length} 条记录(本次新增 ${addedCount} 条)`); + + // 6. 检测需要干预的问题 const { bingshuoIssues, collabIssuesByDev } = detectIssues(allBingshuo, allCollab); console.log(`🔴 冰朔待处理: ${bingshuoIssues.length} 条`); console.log(`🔴 合作者待处理: ${Object.keys(collabIssuesByDev).length} 人\n`); - // 6. 生成各区域内容 + // 7. 生成各区域内容 const bingshuoBulletin = buildBingshuoBulletin(allBingshuo); const bingshuoAlert = buildBingshuoAlert(bingshuoIssues); - const collabBulletin = buildCollabBulletin(allCollab); + const collabBulletin = buildCollabBulletin(cache.records); const collabAlert = buildCollabAlert(collabIssuesByDev); - // 7. 更新 README.md + // 8. 更新 README.md if (!fs.existsSync(README_PATH)) { console.error('❌ README.md 不存在'); process.exit(1); @@ -718,7 +781,7 @@ async function main() { console.log('✅ README.md 公告区已更新'); } - // 8. 发送邮件(如有需要) + // 9. 发送邮件(如有需要) if (bingshuoIssues.length > 0 || Object.keys(collabIssuesByDev).length > 0) { await sendAlertEmails(bingshuoIssues, collabIssuesByDev); }