探索 AI SDK v6:MCP/工具定义/授权审批能力替换范围 #137
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 Governance | |
| on: | |
| issues: | |
| types: [opened, edited, reopened] | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| triage: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Auto-triage labels and duplicate hints | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const issue = context.payload.issue; | |
| const issueNumber = issue.number; | |
| const text = `${issue.title || ""}\n${issue.body || ""}`.toLowerCase(); | |
| const labelDefs = { | |
| "needs-triage": { color: "f9d0c4", description: "Issue needs initial triage" }, | |
| "duplicate-candidate": { color: "cfd3d7", description: "Potential duplicate, needs maintainer confirmation" }, | |
| "area:tui": { color: "1d76db", description: "Terminal UI and interaction layer" }, | |
| "area:tools": { color: "0e8a16", description: "Built-in tools and tool runtime" }, | |
| "area:security": { color: "b60205", description: "Approval, sandbox, and security policy" }, | |
| "area:core": { color: "5319e7", description: "Core runtime and session state" }, | |
| "area:docs": { color: "0075ca", description: "Documentation and docs UX" }, | |
| }; | |
| async function ensureLabel(name) { | |
| try { | |
| await github.rest.issues.getLabel({ owner, repo, name }); | |
| } catch (e) { | |
| if (e.status === 404) { | |
| const d = labelDefs[name]; | |
| if (d) { | |
| await github.rest.issues.createLabel({ owner, repo, name, color: d.color, description: d.description }); | |
| } | |
| } else { | |
| throw e; | |
| } | |
| } | |
| } | |
| const toAdd = new Set(["needs-triage"]); | |
| if (/\[bug\]|\bbug\b/.test(issue.title.toLowerCase())) toAdd.add("bug"); | |
| if (/\[feature\]|\[feat\]|\bfeature\b|\bresearch\b|\brfc\b|\bproposal\b/.test(issue.title.toLowerCase())) toAdd.add("enhancement"); | |
| if (/\bslash\b|\btui\b|\binput\b|\bprompt\b|\bresume\b/.test(text)) toAdd.add("area:tui"); | |
| if (/\btool\b|\bmcp\b|\bskill\b|\bwebfetch\b|\bgrep\b|\bwrite\b|\bedit\b/.test(text)) toAdd.add("area:tools"); | |
| if (/\bsandbox\b|\bapproval\b|\bdangerous\b|\bsecurity\b|\bexec policy\b/.test(text)) toAdd.add("area:security"); | |
| if (/\bcore\b|\bsession\b|\bruntime\b|\barchitecture\b/.test(text)) toAdd.add("area:core"); | |
| if (/\bdoc\b|\breadme\b|\bdocumentation\b/.test(text)) toAdd.add("area:docs"); | |
| for (const name of toAdd) { | |
| await ensureLabel(name); | |
| } | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| labels: [...toAdd], | |
| }); | |
| // Duplicate hint (non-destructive): token-overlap scoring against open issues. | |
| const stopwords = new Set([ | |
| "the", "and", "for", "with", "that", "this", "from", "into", "memo", "cli", "tui", | |
| "feature", "bug", "review", "research", "issue", "openai", "codex", "支持", "新增", "问题" | |
| ]); | |
| function tokenize(s) { | |
| return new Set( | |
| s | |
| .toLowerCase() | |
| .replace(/[^\p{L}\p{N}\s]/gu, " ") | |
| .split(/\s+/) | |
| .map(t => t.trim()) | |
| .filter(t => t.length >= 2 && !stopwords.has(t)) | |
| ); | |
| } | |
| function jaccard(a, b) { | |
| if (!a.size || !b.size) return 0; | |
| let inter = 0; | |
| for (const t of a) if (b.has(t)) inter++; | |
| const uni = new Set([...a, ...b]).size; | |
| return inter / uni; | |
| } | |
| const currentTokens = tokenize(`${issue.title} ${issue.body || ""}`); | |
| const openIssues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner, | |
| repo, | |
| state: "open", | |
| per_page: 100, | |
| }); | |
| const matches = openIssues | |
| .filter(i => !i.pull_request && i.number !== issueNumber) | |
| .map(i => { | |
| const tokens = tokenize(`${i.title} ${i.body || ""}`); | |
| return { | |
| number: i.number, | |
| title: i.title, | |
| url: i.html_url, | |
| score: jaccard(currentTokens, tokens), | |
| }; | |
| }) | |
| .filter(i => i.score >= 0.35) | |
| .sort((a, b) => b.score - a.score) | |
| .slice(0, 3); | |
| if (matches.length > 0) { | |
| await ensureLabel("duplicate-candidate"); | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| labels: ["duplicate-candidate"], | |
| }); | |
| const marker = "<!-- issue-governance:duplicate-check -->"; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| per_page: 100, | |
| }); | |
| const alreadyCommented = comments.some(c => (c.body || "").includes(marker)); | |
| if (!alreadyCommented) { | |
| const lines = matches.map(m => `- #${m.number} (${m.score.toFixed(2)}): ${m.title}\n ${m.url}`); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| body: `${marker}\nPotential duplicates found:\n${lines.join("\n")}\n\nMaintainer: if this is duplicate, close with reason \`duplicate\` and link the canonical issue.`, | |
| }); | |
| } | |
| } |