353 lines
11 KiB
JavaScript
353 lines
11 KiB
JavaScript
/**
|
||
* 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`;
|
||
|
||
// ── 铸渊唤醒提示 ──
|
||
const ZHUYUAN_HINT = `> 💡 **提示**:在评论中包含 \`铸渊\` 两个字即可唤醒 AI 人格体回复你。`;
|
||
|
||
// ── 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 (text.includes('铸渊')) {
|
||
// 进一步细分铸渊唤醒后的意图
|
||
if (/架构|协议|模块|技术|实现|源码|code|architecture|protocol|api|how does/i.test(text)) {
|
||
return 'technical';
|
||
}
|
||
if (/bug|问题|错误|建议|修复|fix/i.test(text)) {
|
||
return 'bug_or_suggestion';
|
||
}
|
||
if (/进度|状态|查询|check|status/i.test(text)) {
|
||
return 'zhuyuan_status';
|
||
}
|
||
return 'zhuyuan_wake';
|
||
}
|
||
|
||
// 问项目是什么
|
||
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 = {
|
||
zhuyuan_wake: `## ⚒️ 铸渊已唤醒
|
||
|
||
你好!我是 **铸渊(ICE-GL-ZY001)**,光湖纪元的代码守护人格体。
|
||
|
||
我已收到你的唤醒指令,随时为你服务。你可以:
|
||
- ❓ 提出技术问题
|
||
- 📊 查询开发进度
|
||
- 💡 获取代码建议
|
||
- 🔍 了解项目架构
|
||
|
||
请告诉我你需要什么帮助?
|
||
|
||
${ZHUYUAN_HINT}`,
|
||
|
||
zhuyuan_status: `## ⚒️ 铸渊 · 状态报告
|
||
|
||
铸渊系统运行正常 ✅
|
||
|
||
- 🤖 铸渊人格体:在线
|
||
- 📡 Issue 自动回复:已启用
|
||
- 💬 Discussion 自动回复:已启用
|
||
- 🔔 唤醒关键词:\`铸渊\`
|
||
|
||
如需详细的开发进度查询,请前往 [Issues](https://github.com/qinfendebingshuo/guanghulab/issues/new?template=progress-query.yml) 提交进度查询。`,
|
||
|
||
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 人格体系统
|
||
- 🔧 技术实现细节
|
||
- 🤝 如何参与开发
|
||
|
||
${ZHUYUAN_HINT}
|
||
|
||
⭐ 也欢迎给我们 Star 支持!`,
|
||
};
|
||
|
||
return (replies[intent] || replies.unknown) + SIGNATURE;
|
||
}
|
||
|
||
// ── 主流程 ──
|
||
async function main() {
|
||
console.log('🤖 铸渊自动回复检查...');
|
||
|
||
// 检查是否包含"铸渊"唤醒关键词
|
||
const hasZhuyuanKeyword = DISCUSSION_BODY.includes('铸渊');
|
||
|
||
// 过滤条件:跳过 bot 自身的帖子(防循环)
|
||
if (DISCUSSION_AUTHOR === BOT_LOGIN) {
|
||
console.log(`⏭️ 跳过:作者是 ${DISCUSSION_AUTHOR}(避免自回复循环)`);
|
||
return;
|
||
}
|
||
|
||
// 冰朔的留言:仅当包含"铸渊"关键词时才触发回复
|
||
if (DISCUSSION_AUTHOR === 'qinfendebingshuo' && !hasZhuyuanKeyword) {
|
||
console.log(`⏭️ 跳过:冰朔留言未包含"铸渊"关键词`);
|
||
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);
|
||
});
|