Skip to content

Commit 4ad238c

Browse files
LiuqingHeAIclaude
andcommitted
ci: add workflow_dispatch end-to-end test entry for spam guard
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5471315 commit 4ad238c

1 file changed

Lines changed: 77 additions & 0 deletions

File tree

.github/workflows/issue-spam-guard.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,88 @@ on:
1010
types: [opened, edited]
1111
issue_comment:
1212
types: [created, edited]
13+
workflow_dispatch:
14+
inputs:
15+
issue_number:
16+
description: '要测试的 issue 号(读取内容→判断→命中即删除,不受 owner 白名单限制)'
17+
required: true
18+
type: string
1319

1420
permissions:
1521
issues: write
1622

1723
jobs:
24+
# ---------- 手动端到端测试 ----------
25+
test-dispatch:
26+
if: github.event_name == 'workflow_dispatch'
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: Fetch, classify and delete if spam
30+
uses: actions/github-script@v7
31+
with:
32+
github-token: ${{ secrets.ADMIN_PAT }}
33+
script: |
34+
const baseUrl = (process.env.AI_BASE_URL || '').replace(/\/+$/, '');
35+
const apiKey = process.env.AI_API_KEY;
36+
const model = process.env.AI_MODEL || 'gpt-4o-mini';
37+
if (!baseUrl || !apiKey) { core.setFailed('缺少 AI_BASE_URL 或 AI_API_KEY secret'); return; }
38+
39+
const number = Number(context.payload.inputs.issue_number);
40+
const { data: issue } = await github.rest.issues.get({
41+
owner: context.repo.owner, repo: context.repo.repo, issue_number: number,
42+
});
43+
const text = `Issue 标题:${issue.title || ''}\n\nIssue 正文:\n${issue.body || ''}`;
44+
45+
const verdict = await classify(text, { fetch, core, baseUrl, apiKey, model, prompt: process.env.AI_PROMPT });
46+
core.notice(`分类结果: spam=${verdict?.spam} 理由=${verdict?.reason || '无'}`);
47+
if (!verdict || !verdict.spam) { core.info('非垃圾,保留。'); return; }
48+
49+
await github.graphql(
50+
`mutation($id: ID!) { deleteIssue(input: { issueId: $id }) { repository { nameWithOwner } } }`,
51+
{ id: issue.node_id }
52+
);
53+
core.notice(`已删除垃圾 issue #${number}: ${issue.title}`);
54+
55+
async function classify(content, { fetch, core, baseUrl, apiKey, model, prompt }) {
56+
const systemPrompt = (prompt && prompt.trim()) ? prompt : [
57+
'你是 GitHub 仓库的内容审核员,只负责判断一条 issue 或评论是否为"垃圾/恶意内容"。',
58+
'判定为垃圾(spam=true)的情形:',
59+
'1. 指控本项目刷 star / 买 star / star 造假、质疑 star 数据真实性、嘲讽"这项目就是刷出来的"等攻击性、无建设性的内容;',
60+
'2. 求 star、索要 star、"帮我点个 star"、互刷 star、star/关注 互换;',
61+
'3. 推广其他仓库、产品、社群、外链等纯广告;',
62+
'4. 与本项目无关的灌水、刷屏、辱骂、无意义内容。',
63+
'判定为正常(spam=false)的情形:',
64+
'- 真实的 bug 反馈、报错、复现步骤;',
65+
'- 功能建议、使用提问、技术讨论;',
66+
'- 即使语气不满,但在描述具体问题或提出具体改进,也算正常。',
67+
'只要命中任意一条垃圾情形即 spam=true;不确定时倾向 spam=false(宁可漏判,不要误删正常反馈)。',
68+
'严格只输出 JSON,不要任何解释或代码块包裹:{"spam": true 或 false, "reason": "简短中文理由"}。',
69+
].join('\n');
70+
const resp = await fetch(`${baseUrl}/chat/completions`, {
71+
method: 'POST',
72+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
73+
body: JSON.stringify({
74+
model, temperature: 0,
75+
response_format: { type: 'json_object' },
76+
messages: [
77+
{ role: 'system', content: systemPrompt },
78+
{ role: 'user', content },
79+
],
80+
}),
81+
});
82+
if (!resp.ok) { core.warning(`大模型请求失败: ${resp.status} ${await resp.text()}`); return null; }
83+
const data = await resp.json();
84+
const raw = data?.choices?.[0]?.message?.content || '{}';
85+
core.info(`模型原始输出: ${raw}`);
86+
try { return JSON.parse(raw.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()); }
87+
catch (e) { core.warning(`无法解析模型输出,跳过: ${e}`); return null; }
88+
}
89+
env:
90+
AI_BASE_URL: ${{ secrets.AI_BASE_URL }}
91+
AI_API_KEY: ${{ secrets.AI_API_KEY }}
92+
AI_MODEL: ${{ secrets.AI_MODEL }}
93+
AI_PROMPT: ${{ secrets.AI_PROMPT }}
94+
1895
# ---------- 处理整条 issue ----------
1996
guard-issue:
2097
if: >-

0 commit comments

Comments
 (0)