|
|
@@ -5,6 +5,8 @@ on:
|
|
|
types: [opened]
|
|
|
issue_comment:
|
|
|
types: [created]
|
|
|
+ pull_request:
|
|
|
+ types: [opened]
|
|
|
pull_request_review_comment:
|
|
|
types: [created]
|
|
|
|
|
|
@@ -30,67 +32,170 @@ jobs:
|
|
|
enable-link-spam-detection: true
|
|
|
enable-ai-detection: true
|
|
|
|
|
|
- # 账号年龄检查(仅针对新建 Issue)
|
|
|
- - name: Account age check
|
|
|
- if: github.event_name == 'issues'
|
|
|
+ # 两层防护:
|
|
|
+ # 1) 账号 < 7 天 → 直接删除/关闭
|
|
|
+ # 2) 账号 >= 7 天 + AI 判定为垃圾 → 也删除/关闭
|
|
|
+ - name: Spam defense
|
|
|
uses: actions/github-script@v7
|
|
|
with:
|
|
|
script: |
|
|
|
- const issue = context.payload.issue;
|
|
|
- const username = issue.user.login;
|
|
|
+ const MIN_ACCOUNT_AGE_DAYS = 7;
|
|
|
+ const event = context.eventName;
|
|
|
+
|
|
|
+ // 解析事件上下文
|
|
|
+ let username, authorAssociation, number, nodeId, commentId;
|
|
|
+ let isIssue = false, isPR = false, isComment = false;
|
|
|
+
|
|
|
+ if (event === 'issues') {
|
|
|
+ const issue = context.payload.issue;
|
|
|
+ username = issue.user.login;
|
|
|
+ authorAssociation = issue.author_association;
|
|
|
+ number = issue.number;
|
|
|
+ isIssue = true;
|
|
|
+ } else if (event === 'issue_comment') {
|
|
|
+ const comment = context.payload.comment;
|
|
|
+ username = comment.user.login;
|
|
|
+ authorAssociation = comment.author_association;
|
|
|
+ number = context.payload.issue.number;
|
|
|
+ nodeId = comment.node_id;
|
|
|
+ commentId = comment.id;
|
|
|
+ isComment = true;
|
|
|
+ isIssue = true;
|
|
|
+ } else if (event === 'pull_request') {
|
|
|
+ const pr = context.payload.pull_request;
|
|
|
+ username = pr.user.login;
|
|
|
+ authorAssociation = pr.author_association;
|
|
|
+ number = pr.number;
|
|
|
+ isPR = true;
|
|
|
+ } else if (event === 'pull_request_review_comment') {
|
|
|
+ const comment = context.payload.comment;
|
|
|
+ username = comment.user.login;
|
|
|
+ authorAssociation = comment.author_association;
|
|
|
+ number = context.payload.pull_request.number;
|
|
|
+ nodeId = comment.node_id;
|
|
|
+ commentId = comment.id;
|
|
|
+ isComment = true;
|
|
|
+ isPR = true;
|
|
|
+ }
|
|
|
|
|
|
// 跳过项目成员
|
|
|
- if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(issue.author_association)) {
|
|
|
- core.info(`Skip check for ${username} (${issue.author_association})`);
|
|
|
+ if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(authorAssociation)) {
|
|
|
+ core.info(`Skip: ${username} (${authorAssociation})`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 获取用户信息
|
|
|
+ // 获取账号年龄
|
|
|
const { data: user } = await github.rest.users.getByUsername({ username });
|
|
|
const accountAgeDays = Math.floor((Date.now() - new Date(user.created_at)) / 86400000);
|
|
|
- const isNewAccount = accountAgeDays < 30;
|
|
|
+ const isNewAccount = accountAgeDays < MIN_ACCOUNT_AGE_DAYS;
|
|
|
+
|
|
|
+ // 判断是否需要拦截
|
|
|
+ let shouldBlock = false;
|
|
|
+ let reason = '';
|
|
|
+
|
|
|
+ if (isNewAccount) {
|
|
|
+ // 第一层:新账号直接拦截
|
|
|
+ shouldBlock = true;
|
|
|
+ reason = `new account (${accountAgeDays}d < ${MIN_ACCOUNT_AGE_DAYS}d)`;
|
|
|
+ } else {
|
|
|
+ // 第二层:老账号检查 AI 垃圾判定
|
|
|
+ let isSpam = false;
|
|
|
+ if (isComment) {
|
|
|
+ const { node } = await github.graphql(`
|
|
|
+ query($id: ID!) {
|
|
|
+ node(id: $id) {
|
|
|
+ ... on IssueComment { isMinimized }
|
|
|
+ ... on PullRequestReviewComment { isMinimized }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ `, { id: nodeId });
|
|
|
+ isSpam = node?.isMinimized === true;
|
|
|
+ } else {
|
|
|
+ const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
|
|
+ ...context.repo,
|
|
|
+ issue_number: number
|
|
|
+ });
|
|
|
+ isSpam = labels.some(l => l.name === 'spam');
|
|
|
+ }
|
|
|
|
|
|
- if (!isNewAccount) {
|
|
|
- core.info(`${username} account age: ${accountAgeDays} days, OK`);
|
|
|
+ if (isSpam) {
|
|
|
+ shouldBlock = true;
|
|
|
+ reason = `spam detected (account age: ${accountAgeDays}d)`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!shouldBlock) {
|
|
|
+ core.info(`${username} (${accountAgeDays}d): OK`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 检查 AI moderator 是否已打 spam 标签
|
|
|
- const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
|
|
- ...context.repo,
|
|
|
- issue_number: issue.number
|
|
|
- });
|
|
|
- const hasSpamLabel = labels.some(l => l.name === 'spam');
|
|
|
+ core.info(`Blocking ${event} #${number} from ${username}: ${reason}`);
|
|
|
|
|
|
- // 两个条件都满足:新账号 + AI 判定为垃圾 → 关闭 + 锁定
|
|
|
- if (isNewAccount && hasSpamLabel) {
|
|
|
- await github.rest.issues.createComment({
|
|
|
- ...context.repo,
|
|
|
- issue_number: issue.number,
|
|
|
- body: [
|
|
|
- '👋 你好,感谢你对本项目的关注!',
|
|
|
+ // 根据拦截原因生成不同提示
|
|
|
+ const notice = isNewAccount
|
|
|
+ ? [
|
|
|
+ `@${username} 你好,感谢你对本项目的关注!`,
|
|
|
'',
|
|
|
- '为维护社区环境、防止垃圾信息和自动化 bot 的干扰,我们对 Issue 进行了自动审核。',
|
|
|
- '很遗憾,你的 Issue 未能通过审核(账号注册时间较短且内容触发了自动检测)。',
|
|
|
+ '为维护社区环境、防止垃圾信息和自动化 bot 的干扰,',
|
|
|
+ `本项目要求 GitHub 账号注册满 **${MIN_ACCOUNT_AGE_DAYS} 天**后才能参与互动。`,
|
|
|
'',
|
|
|
- '**如有误判,还请见谅!** 你可以在账号满 30 天后重新提交,或通过项目 README 中的联系方式反馈。',
|
|
|
+ `你的账号注册时间为 ${accountAgeDays} 天,暂时无法参与。`,
|
|
|
+ `请在账号满 ${MIN_ACCOUNT_AGE_DAYS} 天后重新提交,或通过项目 README 中的联系方式反馈。`,
|
|
|
'',
|
|
|
'感谢理解与支持 🙏',
|
|
|
].join('\n')
|
|
|
- });
|
|
|
+ : [
|
|
|
+ `@${username} 你好,`,
|
|
|
+ '',
|
|
|
+ '你的内容已被自动审核系统标记为垃圾信息并移除。',
|
|
|
+ '',
|
|
|
+ '**如有误判,还请见谅!** 请通过项目 README 中的联系方式反馈。',
|
|
|
+ '',
|
|
|
+ '感谢理解 🙏',
|
|
|
+ ].join('\n');
|
|
|
|
|
|
+ // 执行拦截操作
|
|
|
+ if (isComment) {
|
|
|
+ // 评论:直接删除
|
|
|
+ if (isIssue) {
|
|
|
+ await github.rest.issues.deleteComment({
|
|
|
+ ...context.repo,
|
|
|
+ comment_id: commentId
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ await github.rest.pulls.deleteReviewComment({
|
|
|
+ ...context.repo,
|
|
|
+ comment_id: commentId
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else if (isIssue) {
|
|
|
+ // 新建 Issue:评论提示 → 关闭 → 锁定
|
|
|
+ await github.rest.issues.createComment({
|
|
|
+ ...context.repo,
|
|
|
+ issue_number: number,
|
|
|
+ body: notice
|
|
|
+ });
|
|
|
await github.rest.issues.update({
|
|
|
...context.repo,
|
|
|
- issue_number: issue.number,
|
|
|
+ issue_number: number,
|
|
|
state: 'closed',
|
|
|
state_reason: 'not_planned'
|
|
|
});
|
|
|
-
|
|
|
await github.rest.issues.lock({
|
|
|
...context.repo,
|
|
|
- issue_number: issue.number,
|
|
|
+ issue_number: number,
|
|
|
lock_reason: 'spam'
|
|
|
});
|
|
|
-
|
|
|
- core.info(`Issue #${issue.number} closed: new account (${accountAgeDays}d) + spam detected`);
|
|
|
+ } else if (isPR) {
|
|
|
+ // 新建 PR:评论提示 → 关闭
|
|
|
+ await github.rest.issues.createComment({
|
|
|
+ ...context.repo,
|
|
|
+ issue_number: number,
|
|
|
+ body: notice
|
|
|
+ });
|
|
|
+ await github.rest.pulls.update({
|
|
|
+ ...context.repo,
|
|
|
+ pull_number: number,
|
|
|
+ state: 'closed'
|
|
|
+ });
|
|
|
}
|