Sfoglia il codice sorgente

feat(ci): Issue Guard 增强为两层防护,覆盖 Issue/评论/PR 全场景

sansan 1 mese fa
parent
commit
6d82f2fcc8
1 ha cambiato i file con 138 aggiunte e 33 eliminazioni
  1. 138 33
      .github/workflows/issue-guard.yml

+ 138 - 33
.github/workflows/issue-guard.yml

@@ -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'
+              });
             }