mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 14:41:23 +08:00
docs: 补全私人漫游模式文档内容
This commit is contained in:
@@ -1,204 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const eventName = process.env.GITHUB_EVENT_NAME
|
||||
const eventPath = process.env.GITHUB_EVENT_PATH
|
||||
const token = process.env.GITHUB_TOKEN
|
||||
const repo = process.env.GITHUB_REPOSITORY
|
||||
const model = process.env.MODEL || 'opencode/deepseek-v4-flash-free'
|
||||
const botLogin = process.env.BOT_LOGIN || 'takanashi-hoshino-agent[bot]'
|
||||
|
||||
if (!eventPath || !token || !repo) {
|
||||
console.error(
|
||||
'Missing required env: GITHUB_EVENT_PATH / GITHUB_TOKEN / GITHUB_REPOSITORY',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!process.env.OPENCODE_API_KEY) {
|
||||
console.error('Missing OPENCODE_API_KEY')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const event = JSON.parse(readFileSync(eventPath, 'utf8'))
|
||||
const [owner, repoName] = repo.split('/')
|
||||
const api = `https://api.github.com/repos/${owner}/${repoName}`
|
||||
const headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'opencode-automation',
|
||||
}
|
||||
|
||||
async function get(path) {
|
||||
const res = await fetch(`${api}${path}`, { headers })
|
||||
if (!res.ok) throw new Error(`GET ${path}: HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
async function post(path, body) {
|
||||
const res = await fetch(`${api}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const text = await res.text()
|
||||
if (!res.ok) throw new Error(`POST ${path}: HTTP ${res.status} ${text}`)
|
||||
return text ? JSON.parse(text) : null
|
||||
}
|
||||
|
||||
/** 无头模式跑 opencode,返回模型输出的文本 */
|
||||
function runOpencode(prompt, contextFile) {
|
||||
const args = ['run', '--auto', '-m', model, '--format', 'default', prompt]
|
||||
if (contextFile) args.push('-f', contextFile)
|
||||
console.log(
|
||||
`Running: opencode run --auto -m ${model} (prompt ${prompt.length} chars${contextFile ? ', context attached' : ''})`,
|
||||
)
|
||||
const res = spawnSync('opencode', args, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 128 * 1024 * 1024,
|
||||
})
|
||||
if (res.error) throw res.error
|
||||
if (res.status !== 0) {
|
||||
if (res.stderr) console.error('opencode stderr:\n' + res.stderr)
|
||||
throw new Error(`opencode exited with code ${res.status}`)
|
||||
}
|
||||
return (res.stdout || '').trim()
|
||||
}
|
||||
|
||||
/** 该 issue/PR 是否已有 bot 评论(用于去重,避免 synchronize 重复评论) */
|
||||
async function hasBotComment(target) {
|
||||
const comments = await get(`/issues/${target}/comments?per_page=100`)
|
||||
return comments.some((c) => c.user?.login === botLogin)
|
||||
}
|
||||
|
||||
async function comment(target, body) {
|
||||
if (!body) {
|
||||
console.log('No output from agent, skip comment')
|
||||
return
|
||||
}
|
||||
if (body.length > 60000) body = body.slice(0, 60000) + '\n\n_...(truncated)_'
|
||||
await post(`/issues/${target}/comments`, { body })
|
||||
console.log(`Commented on #${target}`)
|
||||
}
|
||||
|
||||
async function handleIssue(issue) {
|
||||
const n = issue.number
|
||||
console.log(`Handling issue #${n}`)
|
||||
if (await hasBotComment(n)) {
|
||||
console.log(`Already handled by bot, skip`)
|
||||
return
|
||||
}
|
||||
|
||||
const comments = await get(`/issues/${n}/comments?per_page=100`)
|
||||
const thread = comments
|
||||
.filter((c) => c.user?.login !== botLogin)
|
||||
.map(
|
||||
(c) =>
|
||||
`- **${c.user?.login}** (${c.created_at}):\n ${(c.body || '').replace(/\n/g, '\n ')}`,
|
||||
)
|
||||
.join('\n')
|
||||
|
||||
const ctx = [
|
||||
`# Issue #${n}: ${issue.title}`,
|
||||
``,
|
||||
`- State: ${issue.state}`,
|
||||
`- Author: ${issue.user?.login}`,
|
||||
`- Created: ${issue.created_at}`,
|
||||
``,
|
||||
`## Body`,
|
||||
``,
|
||||
issue.body || '(empty)',
|
||||
...(thread ? [`\n## Comments\n\n${thread}`] : []),
|
||||
].join('\n')
|
||||
|
||||
const ctxFile = join(tmpdir(), 'opencode-issue-context.md')
|
||||
writeFileSync(ctxFile, ctx)
|
||||
|
||||
const prompt =
|
||||
process.env.ISSUE_PROMPT ||
|
||||
[
|
||||
`You are a maintainer bot for the ${repo} repository.`,
|
||||
`Read the attached file for the issue context.`,
|
||||
`If the issue has a clear fix or points to relevant docs, reply with documentation links and/or`,
|
||||
`error-handling guidance for code examples. If nothing actionable, reply with exactly "NO_ACTION".`,
|
||||
`Do NOT modify any files, do NOT commit, do NOT push. Output only the comment text.`,
|
||||
].join('\n')
|
||||
|
||||
const output = runOpencode(prompt, ctxFile)
|
||||
if (output === 'NO_ACTION' || output.startsWith('NO_ACTION')) {
|
||||
console.log('Agent decided no action needed')
|
||||
return
|
||||
}
|
||||
await comment(n, output)
|
||||
}
|
||||
|
||||
async function handlePullRequest(pr) {
|
||||
const n = pr.number
|
||||
console.log(`Handling PR #${n}`)
|
||||
if (await hasBotComment(n)) {
|
||||
console.log(`Already handled by bot, skip`)
|
||||
return
|
||||
}
|
||||
|
||||
let files = []
|
||||
try {
|
||||
files = await get(`/pulls/${n}/files?per_page=100`)
|
||||
} catch (e) {
|
||||
console.warn(`Failed to fetch changed files: ${e.message}`)
|
||||
}
|
||||
const fileList = files
|
||||
.map((f) => `- ${f.status} ${f.filename} (+${f.additions}/-${f.deletions})`)
|
||||
.join('\n')
|
||||
|
||||
const ctx = [
|
||||
`# PR #${n}: ${pr.title}`,
|
||||
``,
|
||||
`- State: ${pr.state}`,
|
||||
`- Author: ${pr.user?.login}`,
|
||||
`- Base: ${pr.base?.ref} <- Head: ${pr.head?.ref}`,
|
||||
`- Created: ${pr.created_at}`,
|
||||
`- Stats: +${pr.additions}/-${pr.deletions}, ${pr.changed_files} files`,
|
||||
``,
|
||||
`## Body`,
|
||||
``,
|
||||
pr.body || '(empty)',
|
||||
...(fileList ? [`\n## Changed files\n\n${fileList}`] : []),
|
||||
].join('\n')
|
||||
|
||||
const ctxFile = join(tmpdir(), 'opencode-pr-context.md')
|
||||
writeFileSync(ctxFile, ctx)
|
||||
|
||||
const prompt =
|
||||
process.env.PR_PROMPT ||
|
||||
[
|
||||
`You are a maintainer bot for the ${repo} repository.`,
|
||||
`Read the attached file for the PR context. The PR branch is already checked out.`,
|
||||
`Run \`git diff HEAD^1 HEAD\` (or \`git diff origin/${pr.base?.ref}...HEAD\`) to see the changes.`,
|
||||
`Review the code changes: point out bugs, risks and improvements, and suggest fixes for obvious issues.`,
|
||||
`If the PR looks good, say so concisely.`,
|
||||
`Do NOT modify any files, do NOT commit, do NOT push. Output only the review comment text.`,
|
||||
].join('\n')
|
||||
|
||||
const output = runOpencode(prompt, ctxFile)
|
||||
await comment(n, output)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Event: ${eventName} on ${repo}`)
|
||||
if (eventName === 'issues') {
|
||||
await handleIssue(event.issue)
|
||||
} else if (eventName === 'pull_request') {
|
||||
await handlePullRequest(event.pull_request)
|
||||
} else {
|
||||
console.log(`Unhandled event type: ${eventName}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(`Automation failed: ${e.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user