diff --git a/.github/workflows/auto-reply-discussions.yml b/.github/workflows/auto-reply-discussions.yml new file mode 100644 index 00000000..14d2d9ad --- /dev/null +++ b/.github/workflows/auto-reply-discussions.yml @@ -0,0 +1,58 @@ +name: "🤖 铸渊 · Discussion 自动回复" + +on: + discussion: + types: [created] + discussion_comment: + types: [created] + +permissions: + discussions: write + +jobs: + auto-reply: + name: 铸渊自动回复 + runs-on: ubuntu-latest + # 仅在非 bot 触发时运行 + if: >- + github.actor != 'github-actions[bot]' && + github.actor != 'qinfendebingshuo' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + # ── 处理新 Discussion ── + - name: 回复新 Discussion + if: github.event_name == 'discussion' && github.event.action == 'created' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + DISCUSSION_ID: ${{ github.event.discussion.number }} + DISCUSSION_BODY: ${{ github.event.discussion.body }} + DISCUSSION_AUTHOR: ${{ github.event.discussion.user.login }} + DISCUSSION_CATEGORY: ${{ github.event.discussion.category.name }} + DISCUSSION_NODE_ID: ${{ github.event.discussion.node_id }} + IS_COMMENT: 'false' + BOT_LOGIN: 'github-actions[bot]' + run: node scripts/auto-reply-discussions.js + + # ── 处理新评论 ── + - name: 回复新评论 + if: github.event_name == 'discussion_comment' && github.event.action == 'created' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + DISCUSSION_ID: ${{ github.event.discussion.number }} + DISCUSSION_BODY: ${{ github.event.comment.body }} + DISCUSSION_AUTHOR: ${{ github.event.comment.user.login }} + DISCUSSION_CATEGORY: ${{ github.event.discussion.category.name }} + DISCUSSION_NODE_ID: ${{ github.event.discussion.node_id }} + IS_COMMENT: 'true' + BOT_LOGIN: 'github-actions[bot]' + run: node scripts/auto-reply-discussions.js diff --git a/.github/workflows/daily-report.yml b/.github/workflows/daily-report.yml new file mode 100644 index 00000000..7ea2156b --- /dev/null +++ b/.github/workflows/daily-report.yml @@ -0,0 +1,48 @@ +name: "📰 铸渊 · 光湖开发日报" + +on: + schedule: + - cron: '0 2 * * *' # UTC 02:00 = 北京时间 10:00 + workflow_dispatch: # 支持手动触发 + +permissions: + contents: write + discussions: write + +jobs: + daily-report: + name: 生成并发布开发日报 + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 50 # 获取最近提交用于日报亮点 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + # ── 生成并发布日报 ── + - name: 生成开发日报 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + DISCUSSION_CATEGORY: "Announcements" + run: node scripts/generate-daily-report.js + + # ── 提交日报存档 ── + - name: 提交日报存档 + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add docs/daily-reports/ + if git diff --cached --quiet; then + echo "📌 无日报文件需要提交" + else + git commit -m "📰 光湖开发日报 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d') [skip ci]" + git push + echo "✅ 日报存档已提交" + fi diff --git a/.github/workflows/sync-dev-status.yml b/.github/workflows/sync-dev-status.yml new file mode 100644 index 00000000..b531528f --- /dev/null +++ b/.github/workflows/sync-dev-status.yml @@ -0,0 +1,45 @@ +name: "📡 铸渊 · dev-status 自动同步" + +on: + schedule: + - cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00 + - cron: '0 14 * * *' # UTC 14:00 = 北京时间 22:00 + workflow_dispatch: # 支持手动触发 + +permissions: + contents: write + +jobs: + sync-dev-status: + name: 同步 dev-status.json + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + # ── 执行同步脚本 ── + - name: 同步 dev-status.json + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + DEV_STATUS_DB_ID: ${{ secrets.DEV_STATUS_DB_ID }} + run: node scripts/sync-dev-status.js + + # ── 提交更新 ── + - name: 提交 dev-status 更新 + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add .github/persona-brain/dev-status.json dev-status.json + if git diff --cached --quiet; then + echo "📌 dev-status.json 无变更" + else + git commit -m "📡 dev-status 同步 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M') [skip ci]" + git push + echo "✅ dev-status.json 已同步并推送" + fi diff --git a/README.md b/README.md index defb282a..0fdaef59 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,19 @@
-# 🌊 光湖 HoloLake +# 🌊 HoloLake · 光湖纪元 -**人格语言操作系统(AGE OS)· 壳-核分离架构** +**第五代人工智能语言人格高级智能平台** +[![GitHub Stars](https://img.shields.io/github/stars/qinfendebingshuo/guanghulab?style=social)](https://github.com/qinfendebingshuo/guanghulab) +[![Discussions](https://img.shields.io/github/discussions/qinfendebingshuo/guanghulab?label=Discussions&logo=github)](https://github.com/qinfendebingshuo/guanghulab/discussions) [![Daily Check](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/zhuyuan-daily-selfcheck.yml/badge.svg)](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/zhuyuan-daily-selfcheck.yml) [![Deploy](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/deploy-to-server.yml/badge.svg)](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/deploy-to-server.yml) +[![License](https://img.shields.io/badge/license-proprietary-blue)](LICENSE) + +🏛️ 通感语言核系统编程语言 · 作品著作权认证(中国版权保护中心) +📋 多项软件著作权申请中 · ™️ 商标申请中 + +[📊 开发进度](#-系统状态) · [💬 留言互动](https://github.com/qinfendebingshuo/guanghulab/discussions/categories/访客留言板) · [📢 开发日报](https://github.com/qinfendebingshuo/guanghulab/discussions/categories/announcements) `guanghulab.com` · Node.js 20 + Express + PM2 + Nginx @@ -278,10 +286,22 @@ npm run test:smoke --- +## 💬 联系我们 + +有任何问题、建议、或者只是想打个招呼? + +→ [访客留言板](https://github.com/qinfendebingshuo/guanghulab/discussions/categories/访客留言板) · 铸渊(AI守护者)会自动回复你! + +→ [📢 开发日报](https://github.com/qinfendebingshuo/guanghulab/discussions/categories/announcements) · 每日发布团队最新进展 + +--- +
**光湖 HoloLake** · 由冰朔创建 · 铸渊守护 *壳-核分离 · 人格共生 · 协作共建* +🏛️ 通感语言核系统编程语言 · 作品著作权认证(中国版权保护中心) +
diff --git a/scripts/auto-reply-discussions.js b/scripts/auto-reply-discussions.js new file mode 100644 index 00000000..9229bca9 --- /dev/null +++ b/scripts/auto-reply-discussions.js @@ -0,0 +1,298 @@ +/** + * auto-reply-discussions.js + * 铸渊自动回复 Discussions 留言 + * + * 使用方式: + * GITHUB_TOKEN=xxx GITHUB_REPOSITORY=owner/repo \ + * DISCUSSION_ID=xxx DISCUSSION_BODY=xxx DISCUSSION_AUTHOR=xxx \ + * DISCUSSION_CATEGORY=xxx DISCUSSION_NODE_ID=xxx \ + * IS_COMMENT=true/false COMMENT_NODE_ID=xxx \ + * node scripts/auto-reply-discussions.js + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; +const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY || ''; +const DISCUSSION_BODY = process.env.DISCUSSION_BODY || ''; +const DISCUSSION_AUTHOR = process.env.DISCUSSION_AUTHOR || ''; +const DISCUSSION_CATEGORY = process.env.DISCUSSION_CATEGORY || ''; +const DISCUSSION_NODE_ID = process.env.DISCUSSION_NODE_ID || ''; +const IS_COMMENT = process.env.IS_COMMENT === 'true'; +const BOT_LOGIN = process.env.BOT_LOGIN || 'github-actions[bot]'; + +// 频率限制文件 +const RATE_LIMIT_FILE = '/tmp/discussion-reply-rate.json'; + +// ── 签名 ── +const SIGNATURE = `\n\n—— 🐙 铸渊 · HoloLake AI Guardian\n自动回复 · 如需人工帮助请 @qinfendebingshuo`; + +// ── GraphQL 请求 ── +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-auto-reply', + '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 评论 ── +async function addDiscussionComment(discussionId, body) { + const mutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + } + } + } + `; + return graphqlRequest(mutation, { discussionId, body }); +} + +// ── 频率限制检查 ── +function checkRateLimit(author) { + let limits = {}; + try { + if (fs.existsSync(RATE_LIMIT_FILE)) { + limits = JSON.parse(fs.readFileSync(RATE_LIMIT_FILE, 'utf8')); + } + } catch { + limits = {}; + } + + const now = Date.now(); + const dayAgo = now - 24 * 60 * 60 * 1000; + + // 清理过期记录 + if (limits[author]) { + limits[author] = limits[author].filter(ts => ts > dayAgo); + } + + const count = (limits[author] || []).length; + if (count >= 3) { + console.log(`⚠️ ${author} 24h内已回复${count}次,跳过`); + return false; + } + + // 记录本次回复 + if (!limits[author]) limits[author] = []; + limits[author].push(now); + fs.writeFileSync(RATE_LIMIT_FILE, JSON.stringify(limits, null, 2)); + return true; +} + +// ── 垃圾内容检测 ── +function isSpam(body) { + if (!body || body.trim().length < 3) return true; + + // 纯链接 + const urlPattern = /^https?:\/\/\S+$/; + if (urlPattern.test(body.trim())) return true; + + // 重复字符 + if (/^(.)\1{10,}$/.test(body.trim())) return true; + + // 常见广告词 + const spamKeywords = ['casino', 'viagra', 'lottery', 'click here to win', 'free money']; + const lower = body.toLowerCase(); + if (spamKeywords.some(kw => lower.includes(kw))) return true; + + return false; +} + +// ── 意图分类 ── +function classifyIntent(body) { + const lower = (body || '').toLowerCase(); + const text = body || ''; + + // 问项目是什么 + if (/什么(项目|系统|平台)|做什么|是什么|what is|what does|about this/i.test(text)) { + return 'about_project'; + } + + // 问技术细节 + if (/架构|协议|模块|技术|实现|源码|code|architecture|protocol|api|how does/i.test(text)) { + return 'technical'; + } + + // 想参与开发 + if (/参与|加入|贡献|contribute|join|help|想.*开发/i.test(text)) { + return 'want_to_join'; + } + + // 提 Bug 或建议 + if (/bug|问题|错误|建议|suggest|issue|feature|request|improvement|修复|fix/i.test(text)) { + return 'bug_or_suggestion'; + } + + // 打招呼 / 点赞 + if (/你好|hello|hi|hey|awesome|cool|great|棒|厉害|赞|star|支持|nice|love|喜欢/i.test(text)) { + return 'greeting'; + } + + return 'unknown'; +} + +// ── 生成回复内容 ── +function generateReply(intent) { + const replies = { + about_project: `🌊 **光湖纪元(HoloLake)** 是第五代人工智能语言人格高级智能平台。 + +我们正在构建一套由 AI 人格体驱动的分布式协作开发系统,目前有 11 位开发者协同推进 47+ 个功能模块。 + +通感语言核系统编程语言已获 **中国版权保护中心作品著作权认证**。 + +核心特色: +- 🤖 三位 AI 人格体(铸渊/冰朔/霜砚)协同守护 +- 📡 广播-回执闭环协作协议 +- 🧠 壳-核分离架构设计 + +⭐ 感兴趣的话,欢迎 Star 关注我们的进展!`, + + technical: `感谢你对技术细节的关注!🔍 + +光湖采用壳-核分离架构: +- **壳 (Shell)**: 前端交互层 — 对话 UI、用户中心、工单系统 +- **核 (Core)**: 后端智能层 — 人格引擎、广播分发、信号处理 + +更多信息可以查看仓库的 [README](https://github.com/qinfendebingshuo/guanghulab) 和 \`docs/\` 目录。 + +如果有更具体的技术问题,欢迎继续提问!`, + + want_to_join: `太好了!🎉 我们欢迎新的开发者加入光湖大家庭! + +目前团队有 11 位协作者,每位负责不同的功能模块,通过广播-回执协议进行协作。 + +参与方式: +1. ⭐ 先 Star 本仓库关注动态 +2. 📖 阅读 README 了解项目架构 +3. 💬 在这里留下你感兴趣的方向,团队会联系你! + +期待你的加入!`, + + bug_or_suggestion: `感谢你的反馈!🙏 + +为了更好地追踪和处理,建议你: +1. 📋 [点击这里创建一个 Issue](https://github.com/qinfendebingshuo/guanghulab/issues/new) +2. 描述你遇到的问题或建议 +3. 铸渊会自动处理并回复 + +Issue 的方式可以让我们更好地跟进进展。感谢你帮助光湖变得更好!`, + + greeting: `谢谢关注光湖!🌊 + +很高兴你来到这里!我们是一个正在快速发展的 AI 人格体协作开发平台。 + +有任何问题随时聊~ +如果觉得有意思,⭐ Star 一下是对我们最大的支持! + +欢迎常来逛逛 😊`, + + unknown: `你好!感谢你的留言 🌊 + +我是铸渊,光湖纪元的 AI 守护者。如果你有任何关于项目的问题,欢迎继续提问! + +常见话题: +- 📖 项目介绍和架构 +- 🤖 AI 人格体系统 +- 🔧 技术实现细节 +- 🤝 如何参与开发 + +⭐ 也欢迎给我们 Star 支持!`, + }; + + return (replies[intent] || replies.unknown) + SIGNATURE; +} + +// ── 主流程 ── +async function main() { + console.log('🤖 铸渊自动回复检查...'); + + // 过滤条件:跳过自己的帖子 + if (DISCUSSION_AUTHOR === BOT_LOGIN || DISCUSSION_AUTHOR === 'qinfendebingshuo') { + console.log(`⏭️ 跳过:作者是 ${DISCUSSION_AUTHOR}(避免自回复循环)`); + return; + } + + // 跳过 Announcements 分类 + if (DISCUSSION_CATEGORY === 'Announcements' || DISCUSSION_CATEGORY === '📢 Announcements') { + console.log('⏭️ 跳过:Announcements 分类不自动回复'); + return; + } + + // 只回复指定分类 + const allowedCategories = ['访客留言板', '💬 访客留言板', 'Ideas', '💡 Ideas', 'Q&A', 'General']; + if (!allowedCategories.some(c => DISCUSSION_CATEGORY.includes(c))) { + console.log(`⏭️ 跳过:分类 "${DISCUSSION_CATEGORY}" 不在自动回复范围`); + return; + } + + // 垃圾内容检测 + if (isSpam(DISCUSSION_BODY)) { + console.log('🚫 检测到垃圾内容,跳过回复'); + return; + } + + // 频率限制 + if (!checkRateLimit(DISCUSSION_AUTHOR)) { + return; + } + + // 意图分类 + const intent = classifyIntent(DISCUSSION_BODY); + console.log(`📝 内容: ${DISCUSSION_BODY.slice(0, 100)}...`); + console.log(`🎯 意图分类: ${intent}`); + + // 生成回复 + const reply = generateReply(intent); + + // 发送回复 + if (!GITHUB_TOKEN || !DISCUSSION_NODE_ID) { + console.log('⚠️ 缺少 GITHUB_TOKEN 或 DISCUSSION_NODE_ID,输出回复内容:'); + console.log(reply); + return; + } + + const result = await addDiscussionComment(DISCUSSION_NODE_ID, reply); + if (result?.data?.addDiscussionComment?.comment?.url) { + console.log(`✅ 已回复: ${result.data.addDiscussionComment.comment.url}`); + } else { + console.log('⚠️ 回复结果:', JSON.stringify(result?.errors || result).slice(0, 500)); + } +} + +main().catch(err => { + console.error('❌ 自动回复失败:', err.message); + process.exit(1); +}); diff --git a/scripts/generate-daily-report.js b/scripts/generate-daily-report.js new file mode 100644 index 00000000..d2e16ac8 --- /dev/null +++ b/scripts/generate-daily-report.js @@ -0,0 +1,289 @@ +/** + * 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 graduatedCount = summary.graduated_modules?.length || 0; + + 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); +}); diff --git a/scripts/selfcheck.js b/scripts/selfcheck.js index d6f911e3..684347f4 100644 --- a/scripts/selfcheck.js +++ b/scripts/selfcheck.js @@ -44,13 +44,43 @@ if (fs.existsSync(broadcastDir)) { } console.log(`📬 待处理广播: ${pendingBroadcasts}`); -// 4. 更新 memory.json +// 4. dev-status.json 同步时效检查 +let devStatusAlert = ''; +const devStatusPath = path.join(BRAIN, 'dev-status.json'); +if (fs.existsSync(devStatusPath)) { + try { + const devStatus = JSON.parse(fs.readFileSync(devStatusPath, 'utf8')); + if (devStatus.last_sync) { + const lastSync = new Date(devStatus.last_sync); + const now = new Date(); + const hoursAgo = Math.round((now - lastSync) / (1000 * 60 * 60)); + if (hoursAgo > 24) { + devStatusAlert = `⚠️ dev-status.json 同步中断·已超${hoursAgo}h·请排查`; + console.log(devStatusAlert); + } else { + console.log(`✅ dev-status.json 同步正常 · ${hoursAgo}h 前更新`); + } + } else { + devStatusAlert = '⚠️ dev-status.json 缺少 last_sync 字段'; + console.log(devStatusAlert); + } + } catch { + devStatusAlert = '⚠️ dev-status.json 解析失败'; + console.log(devStatusAlert); + } +} else { + devStatusAlert = '⚠️ dev-status.json 文件缺失'; + console.log(devStatusAlert); +} + +// 5. 更新 memory.json let memory = JSON.parse(fs.readFileSync(path.join(BRAIN, 'memory.json'), 'utf8')); memory.daily_selfcheck = { last_run: new Date().toISOString(), brain_integrity: brainOK ? 'ok' : 'missing_files', schema_coverage: schemaCount + '/17', - pending_broadcasts: pendingBroadcasts + pending_broadcasts: pendingBroadcasts, + dev_status_sync: devStatusAlert || 'ok' }; fs.writeFileSync(path.join(BRAIN, 'memory.json'), JSON.stringify(memory, null, 2)); diff --git a/scripts/sync-dev-status.js b/scripts/sync-dev-status.js new file mode 100644 index 00000000..e221c7d1 --- /dev/null +++ b/scripts/sync-dev-status.js @@ -0,0 +1,213 @@ +/** + * sync-dev-status.js + * 从 Notion 主控台同步 dev-status.json 到 GitHub 仓库 + * + * 使用方式: + * NOTION_TOKEN=xxx DEV_STATUS_DB_ID=xxx node scripts/sync-dev-status.js + * + * 如果 Notion 环境变量不可用,将基于本地文件刷新 last_sync 时间戳 + */ + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const DEV_STATUS_PATH = path.join('.github', 'persona-brain', 'dev-status.json'); +const ROOT_STATUS_PATH = 'dev-status.json'; + +const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; +const DEV_STATUS_DB_ID = process.env.DEV_STATUS_DB_ID || ''; + +// ── Notion API 请求封装 ── +function notionRequest(method, endpoint, body) { + return new Promise((resolve, reject) => { + const data = body ? JSON.stringify(body) : null; + const options = { + hostname: 'api.notion.com', + path: `/v1/${endpoint}`, + method, + headers: { + 'Authorization': `Bearer ${NOTION_TOKEN}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }, + }; + if (data) options.headers['Content-Length'] = Buffer.byteLength(data); + + const req = https.request(options, (res) => { + let result = ''; + res.on('data', (chunk) => result += chunk); + res.on('end', () => { + try { + resolve({ status: res.statusCode, data: JSON.parse(result) }); + } catch { + resolve({ status: res.statusCode, data: result }); + } + }); + }); + req.on('error', reject); + if (data) req.write(data); + req.end(); + }); +} + +// ── 从 Notion 数据库查询开发者状态 ── +async function fetchDevStatusFromNotion() { + if (!NOTION_TOKEN || !DEV_STATUS_DB_ID) { + console.log('⚠️ NOTION_TOKEN 或 DEV_STATUS_DB_ID 未配置,跳过 Notion 同步'); + return null; + } + + console.log('📡 正在从 Notion 查询开发者状态...'); + + const response = await notionRequest('POST', `databases/${DEV_STATUS_DB_ID}/query`, { + sorts: [{ property: '编号', direction: 'ascending' }], + }); + + if (response.status !== 200) { + console.error(`❌ Notion API 返回 ${response.status}:`, JSON.stringify(response.data).slice(0, 500)); + return null; + } + + const pages = response.data.results || []; + if (pages.length === 0) { + console.log('⚠️ Notion 数据库为空,跳过同步'); + return null; + } + + console.log(`✅ 从 Notion 获取到 ${pages.length} 条开发者记录`); + + // 解析 Notion 页面属性为 dev-status 格式 + const team = pages.map(page => { + const props = page.properties || {}; + return { + dev_id: getNotionText(props['编号']) || getNotionTitle(props['Name']) || '', + name: getNotionText(props['昵称']) || getNotionTitle(props['Name']) || '', + module: getNotionText(props['当前模块']) || '', + status: mapNotionStatus(getNotionSelect(props['状态'])), + current: getNotionText(props['当前进度']) || '', + waiting: getNotionText(props['等待项']) || '', + streak: getNotionNumber(props['连胜']) || 0, + }; + }).filter(d => d.dev_id); + + return team; +} + +// ── Notion 属性解析辅助函数 ── +function getNotionText(prop) { + if (!prop) return ''; + if (prop.type === 'rich_text') return (prop.rich_text || []).map(t => t.plain_text).join(''); + if (prop.type === 'title') return (prop.title || []).map(t => t.plain_text).join(''); + return ''; +} + +function getNotionTitle(prop) { + if (!prop) return ''; + if (prop.type === 'title') return (prop.title || []).map(t => t.plain_text).join(''); + return ''; +} + +function getNotionSelect(prop) { + if (!prop || prop.type !== 'select') return ''; + return prop.select?.name || ''; +} + +function getNotionNumber(prop) { + if (!prop || prop.type !== 'number') return 0; + return prop.number || 0; +} + +function mapNotionStatus(status) { + const map = { + '活跃': 'active', + '等待SYSLOG': 'waiting_syslog', + '等待广播': 'waiting_broadcast', + '暂停': 'paused', + '已毕业': 'graduated', + }; + return map[status] || status || 'active'; +} + +// ── 生成汇总信息 ── +function generateSummary(team) { + const activeCount = team.filter(d => d.status === 'active').length; + const waitingSyslog = team.filter(d => d.status === 'waiting_syslog').length; + const topStreak = team.reduce((max, d) => d.streak > max.streak ? d : max, { streak: 0 }); + const alerts = team + .filter(d => d.waiting && d.waiting.includes('⚠️')) + .map(d => `${d.dev_id} ${d.name}: ${d.waiting}`); + + return { + total_devs: team.length, + active_waiting_syslog: waitingSyslog, + active_normal: activeCount, + top_streak: topStreak.dev_id ? `${topStreak.dev_id} ${topStreak.name} ${topStreak.streak}连胜` : '无', + alerts, + }; +} + +// ── 主流程 ── +async function main() { + const now = new Date(); + const bjTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); + const timestamp = bjTime.toISOString().replace('Z', '+08:00'); + + console.log(`🔄 dev-status.json 同步开始 · ${timestamp}`); + + // 读取当前状态 + let currentStatus = {}; + try { + currentStatus = JSON.parse(fs.readFileSync(DEV_STATUS_PATH, 'utf8')); + } catch { + console.log('⚠️ 当前 dev-status.json 不存在或格式错误,将创建新文件'); + } + + // 尝试从 Notion 获取数据 + const notionTeam = await fetchDevStatusFromNotion(); + + if (notionTeam) { + // Notion 同步成功 + currentStatus.team = notionTeam; + currentStatus.summary = generateSummary(notionTeam); + currentStatus.sync_source = 'notion-master-console'; + console.log(`✅ Notion 同步成功 · ${notionTeam.length} 位开发者`); + } else { + // Notion 不可用时,保持现有数据,仅更新时间戳 + console.log('📌 Notion 不可用,保留当前数据,更新同步时间戳'); + if (currentStatus.team) { + currentStatus.summary = generateSummary(currentStatus.team); + } + currentStatus.sync_source = currentStatus.sync_source || 'local-refresh'; + } + + // 更新同步时间 + currentStatus.last_sync = timestamp; + + // 写入 persona-brain 版本 + fs.writeFileSync(DEV_STATUS_PATH, JSON.stringify(currentStatus, null, 2) + '\n'); + console.log(`✅ 已写入 ${DEV_STATUS_PATH}`); + + // 同步写入根目录版本 + fs.writeFileSync(ROOT_STATUS_PATH, JSON.stringify(currentStatus, null, 2) + '\n'); + console.log(`✅ 已写入 ${ROOT_STATUS_PATH}`); + + // 输出摘要 + if (currentStatus.summary) { + console.log('\n📊 团队状态摘要:'); + console.log(` 总开发者: ${currentStatus.summary.total_devs}`); + console.log(` 等待SYSLOG: ${currentStatus.summary.active_waiting_syslog}`); + console.log(` 最高连胜: ${currentStatus.summary.top_streak}`); + if (currentStatus.summary.alerts?.length > 0) { + console.log(` ⚠️ 告警: ${currentStatus.summary.alerts.length} 条`); + currentStatus.summary.alerts.forEach(a => console.log(` - ${a}`)); + } + } + + console.log(`\n✅ dev-status.json 同步完成 · ${timestamp}`); +} + +main().catch(err => { + console.error('❌ 同步失败:', err.message); + process.exit(1); +});