开发团队是不是弃坑了?4个月没有发版了。。。 #5855
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Issue Spam Filter | |
| on: | |
| issues: | |
| types: [opened, edited] | |
| jobs: | |
| spam-check: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Run spam check script | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const issue = context.payload.issue; | |
| if (!issue) return; | |
| // load config from repo (fallback defaults if missing) | |
| let config = { | |
| dry_run: true, | |
| min_account_age_days: 3, | |
| max_urls_for_spam: 1, | |
| min_body_len_for_links: 40, | |
| spam_words: [ | |
| "call now","zadzwoń","zadzwoń teraz","kontakt","telefon","telefone","contato", | |
| "suporte","infolinii","click here","buy now","subscribe","visit" | |
| ], | |
| bracket_max: 6, | |
| special_char_density_threshold: 0.12, | |
| phone_regex: "\\+?\\d[\\d\\-\\s\\(\\)\\.]{6,}\\d", | |
| labels_for_spam: ["spam"], | |
| labels_for_review: ["needs-triage"] | |
| }; | |
| try { | |
| const cfg = await github.rest.repos.getContent({ owner, repo, path: ".github/issue-spam-config.json" }); | |
| const raw = Buffer.from(cfg.data.content, "base64").toString(); | |
| config = Object.assign(config, JSON.parse(raw)); | |
| } catch (e) { | |
| // allow missing config file — use defaults | |
| } | |
| // Ensure labels exist | |
| async function ensureLabel(name, color, description) { | |
| try { | |
| await github.rest.issues.getLabel({ owner, repo, name }); | |
| } catch (e) { | |
| if (e.status === 404) { | |
| try { | |
| await github.rest.issues.createLabel({ owner, repo, name, color, description }); | |
| } catch (e2) { | |
| // ignore concurrent create errors | |
| } | |
| } | |
| } | |
| } | |
| for (const lb of config.labels_for_spam) { | |
| await ensureLabel(lb, "B60205", "Auto-marked as spam by workflow"); | |
| } | |
| for (const lb of config.labels_for_review) { | |
| await ensureLabel(lb, "FBCA04", "Needs triage"); | |
| } | |
| const title = (issue.title || "").toLowerCase(); | |
| const body = (issue.body || "").toLowerCase(); | |
| const combined = title + "\n" + body; | |
| // helpers | |
| const countMatches = (text, re) => ((text.match(re) || []).length); | |
| const urlRegex = /https?:\/\/\S+/g; | |
| const urlCount = countMatches(combined, urlRegex); | |
| const bodyLenNoSpace = combined.replace(/\s+/g, '').length; | |
| const spamWordsHit = config.spam_words.some(w => combined.includes(w)); | |
| const bracketCount = countMatches(title, /[\{\}\[\]\<\>\|\~\^\_]/g) + countMatches(body, /[\{\}\[\]\<\>\|\~\^\_]/g); | |
| const specialChars = combined.match(/[^a-z0-9\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/gi) || []; | |
| const specialCharDensity = specialChars.length / Math.max(1, combined.length); | |
| const phoneRe = new RegExp(config.phone_regex, "g"); | |
| const phoneCount = countMatches(combined, phoneRe); | |
| // account age | |
| let accountAgeDays = 3650; | |
| try { | |
| if (issue.user && issue.user.login) { | |
| const u = await github.rest.users.getByUsername({ username: issue.user.login }); | |
| accountAgeDays = (Date.now() - new Date(u.data.created_at)) / (1000*60*60*24); | |
| } | |
| } catch (e) { | |
| // ignore user fetch errors | |
| } | |
| // Decision logic (tunable) | |
| let isSpam = false; | |
| let reasons = []; | |
| if (urlCount > config.max_urls_for_spam) { | |
| isSpam = true; | |
| reasons.push(`包含 ${urlCount} 个链接`); | |
| } | |
| if (urlCount >= 1 && accountAgeDays < config.min_account_age_days) { | |
| isSpam = true; | |
| reasons.push(`账号创建 ${Math.floor(accountAgeDays)} 天且包含链接`); | |
| } | |
| if (spamWordsHit) { | |
| isSpam = true; | |
| reasons.push("命中垃圾关键词"); | |
| } | |
| if (bodyLenNoSpace < config.min_body_len_for_links && urlCount >= 1) { | |
| isSpam = true; | |
| reasons.push("正文过短且含链接"); | |
| } | |
| if (bracketCount >= config.bracket_max) { | |
| isSpam = true; | |
| reasons.push(`标题/正文中存在大量特殊括号 (${bracketCount})`); | |
| } | |
| if (specialCharDensity >= config.special_char_density_threshold && combined.length < 200) { | |
| isSpam = true; | |
| reasons.push(`特殊字符密度高 (${(specialCharDensity*100).toFixed(1)}%)`); | |
| } | |
| if (phoneCount >= 1 && bracketCount >= 2) { | |
| isSpam = true; | |
| reasons.push("包含电话号码模式且带有异常符号格式"); | |
| } | |
| // Apply actions | |
| const issue_number = issue.number; | |
| if (isSpam) { | |
| try { | |
| await github.rest.issues.addLabels({ owner, repo, issue_number, labels: config.labels_for_spam }); | |
| } catch (e) { /* ignore */ } | |
| const commentBody = `该 issue 被自动判定为可能的垃圾(${reasons.join(',')})。` + | |
| (config.dry_run ? "当前处于 dry-run 模式,仅已打标签并留言;如需自动关闭,请在配置中关闭 dry_run。" : | |
| "已自动关闭。如误判请联系维护者。"); | |
| await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody }); | |
| if (!config.dry_run) { | |
| await github.rest.issues.update({ owner, repo, issue_number, state: "closed" }); | |
| } | |
| return; | |
| } | |
| try { | |
| await github.rest.issues.addLabels({ owner, repo, issue_number, labels: config.labels_for_review }); | |
| } catch (e) { /* ignore */ } |