返回 DeepSeek-Reasonix
backfill-issue-labels.mjs
根目录 / scripts / backfill-issue-labels.mjs
1 #!/usr/bin/env node
2 // Backfill area/platform/severity labels on existing open issues via the DeepSeek
3 // API — the one-off companion to .github/workflows/issue-auto-label.yml (which
4 // only fires on new issues). Keep the label sets and prompt in sync with that
5 // workflow. GitHub access uses the local `gh` CLI (must be authenticated).
6 //
7 // Usage:
8 // DEEPSEEK_API_KEY=... node scripts/backfill-issue-labels.mjs [options]
9 // Options:
10 // --dry-run print what would change, apply nothing
11 // --only-unlabeled skip issues that already have an area label
12 // --limit N process at most N issues (default 200)
13
14 import { execFileSync } from 'node:child_process';
15
16 const args = process.argv.slice(2);
17 const dryRun = args.includes('--dry-run');
18 const onlyUnlabeled = args.includes('--only-unlabeled');
19 const li = args.indexOf('--limit');
20 const limit = li >= 0 ? parseInt(args[li + 1], 10) : 200;
21
22 const KEY = process.env.DEEPSEEK_API_KEY;
23 if (!KEY) {
24 console.error('DEEPSEEK_API_KEY is not set');
25 process.exit(1);
26 }
27
28 const AREA = ['agent', 'mcp', 'config', 'updater', 'provider', 'desktop', 'tui', 'skills', 'rendering'];
29 const PLATFORM = ['windows', 'macos', 'linux'];
30 const SEVERITY = ['crash', 'data-loss', 'security'];
31 const ALLOWED = new Set([...AREA, ...PLATFORM, ...SEVERITY]);
32
33 const SYSTEM = [
34 'You categorize GitHub issues for Reasonix, a Go-based AI coding agent with a Wails desktop app and a terminal UI.',
35 'Pick labels ONLY from these fixed sets. Never invent labels.',
36 'area (0-2, the affected subsystem):',
37 ' agent: core agent loop / tool-calling / reasoning',
38 ' mcp: MCP servers and plugins',
39 ' config: configuration, setup wizard, .toml/.env',
40 ' updater: auto-update, installer, release packaging',
41 ' provider: model providers, model selection/switching',
42 ' desktop: Wails desktop GUI',
43 ' tui: terminal UI / CLI',
44 ' skills: skills system',
45 ' rendering: terminal rendering / flicker / repaint',
46 'platform (only if clearly specific to one OS): windows, macos, linux',
47 'severity (only if clearly applicable):',
48 ' crash: app crashes, hangs, or freezes',
49 ' data-loss: loss of sessions, config, or history',
50 ' security: credential/secret exposure or a security flaw',
51 'Be conservative: omit a label when unsure. The issue may be in Chinese.',
52 'Reply with JSON only: {"area":[],"platform":[],"severity":[]}',
53 ].join('\n');
54
55 function gh(args) {
56 return execFileSync('gh', args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
57 }
58
59 async function classify(title, body) {
60 const res = await fetch('https://api.deepseek.com/chat/completions', {
61 method: 'POST',
62 headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
63 body: JSON.stringify({
64 model: 'deepseek-chat',
65 temperature: 0,
66 response_format: { type: 'json_object' },
67 messages: [
68 { role: 'system', content: SYSTEM },
69 { role: 'user', content: `Title: ${title}\n\nBody:\n${body}` },
70 ],
71 }),
72 });
73 if (!res.ok) throw new Error(`DeepSeek API ${res.status}: ${await res.text()}`);
74 const data = await res.json();
75 const parsed = JSON.parse(data.choices[0].message.content);
76 return [...(parsed.area || []), ...(parsed.platform || []), ...(parsed.severity || [])].filter((l) => ALLOWED.has(l));
77 }
78
79 const issues = JSON.parse(
80 gh(['issue', 'list', '--state', 'open', '--limit', String(limit), '--json', 'number,title,body,labels']),
81 );
82 console.log(`${issues.length} open issues; dryRun=${dryRun} onlyUnlabeled=${onlyUnlabeled}`);
83
84 let changed = 0;
85 for (const it of issues) {
86 const existing = it.labels.map((l) => l.name);
87 if (onlyUnlabeled && existing.some((l) => AREA.includes(l))) {
88 continue;
89 }
90 let labels;
91 try {
92 labels = await classify(it.title, (it.body || '').slice(0, 4000));
93 } catch (e) {
94 console.warn(`#${it.number}: classify failed: ${e.message}`);
95 continue;
96 }
97 if (!labels.some((l) => AREA.includes(l))) labels.push('needs-triage');
98 const toAdd = labels.filter((l) => !existing.includes(l));
99 if (!toAdd.length) {
100 console.log(`#${it.number}: nothing new`);
101 continue;
102 }
103 if (dryRun) {
104 console.log(`#${it.number}: would add ${toAdd.join(', ')} — ${it.title.slice(0, 50)}`);
105 } else {
106 gh(['issue', 'edit', String(it.number), ...toAdd.flatMap((l) => ['--add-label', l])]);
107 console.log(`#${it.number}: +${toAdd.join(', ')}`);
108 }
109 changed++;
110 }
111 console.log(`Done. ${changed} issue(s) ${dryRun ? 'would be' : ''} updated.`);
112
112 lines Plain Text