feat: dev-status sync, daily report, auto-reply bot, README update
Task 1: dev-status.json sync workflow + script + selfcheck alerting Task 2: Daily report generation workflow + script Task 3: README with badges, certifications, Discussions links, contact section Task 4: Auto-reply bot for Discussions with intent classification Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
6b9c1316ef
commit
77599c4082
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
24
README.md
24
README.md
|
|
@ -1,11 +1,19 @@
|
|||
<div align="center">
|
||||
|
||||
# 🌊 光湖 HoloLake
|
||||
# 🌊 HoloLake · 光湖纪元
|
||||
|
||||
**人格语言操作系统(AGE OS)· 壳-核分离架构**
|
||||
**第五代人工智能语言人格高级智能平台**
|
||||
|
||||
[](https://github.com/qinfendebingshuo/guanghulab)
|
||||
[](https://github.com/qinfendebingshuo/guanghulab/discussions)
|
||||
[](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/zhuyuan-daily-selfcheck.yml)
|
||||
[](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/deploy-to-server.yml)
|
||||
[](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) · 每日发布团队最新进展
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**光湖 HoloLake** · 由冰朔创建 · 铸渊守护
|
||||
|
||||
*壳-核分离 · 人格共生 · 协作共建*
|
||||
|
||||
🏛️ 通感语言核系统编程语言 · 作品著作权认证(中国版权保护中心)
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
Loading…
Reference in New Issue