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);
|
|||
|
|
});
|