This commit is contained in:
马登山
2026-02-27 16:13:21 +08:00
parent ff794d8741
commit 28a5b1b4c1
+111 -25
View File
@@ -3,7 +3,19 @@ import { writeFile } from "node:fs/promises"
const anthropicToken = process.env.ANTHROPIC_AUTH_TOKEN
const anthropicBaseUrl = process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com"
const anthropicModel = process.env.ANTHROPIC_MODEL || "claude-opus-4.1"
const defaultHaikuModel = process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL || "claude-haiku-4.5"
const defaultOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL || "claude-opus-4.6"
const defaultSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL || "claude-sonnet-4.6"
// Priority: explicit model override -> sonnet default -> haiku -> opus.
const anthropicModel = process.env.ANTHROPIC_MODEL || defaultSonnetModel || defaultHaikuModel || defaultOpusModel
const timeoutMs = Number(process.env.ANTHROPIC_TIMEOUT_MS || "180000")
const maxRetries = Number(process.env.ANTHROPIC_MAX_RETRIES || "3")
const retryBaseMs = Number(process.env.ANTHROPIC_RETRY_BASE_MS || "2000")
const maxTokens = Number(process.env.ANTHROPIC_MAX_TOKENS || "2048")
const issueNumber = process.env.ISSUE_NUMBER || ""
const issueTitle = process.env.ISSUE_TITLE || ""
const issueBody = process.env.ISSUE_BODY || ""
@@ -15,6 +27,26 @@ if (!anthropicToken) {
process.exit(1)
}
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
console.error(`Invalid ANTHROPIC_TIMEOUT_MS: ${process.env.ANTHROPIC_TIMEOUT_MS}`)
process.exit(1)
}
if (!Number.isFinite(maxRetries) || maxRetries < 1) {
console.error(`Invalid ANTHROPIC_MAX_RETRIES: ${process.env.ANTHROPIC_MAX_RETRIES}`)
process.exit(1)
}
if (!Number.isFinite(retryBaseMs) || retryBaseMs < 0) {
console.error(`Invalid ANTHROPIC_RETRY_BASE_MS: ${process.env.ANTHROPIC_RETRY_BASE_MS}`)
process.exit(1)
}
if (!Number.isFinite(maxTokens) || maxTokens < 1) {
console.error(`Invalid ANTHROPIC_MAX_TOKENS: ${process.env.ANTHROPIC_MAX_TOKENS}`)
process.exit(1)
}
const prompt = [
"You are a senior software engineer fixing a GitHub issue.",
"Return ONLY strict JSON with keys: summary, patch.",
@@ -29,33 +61,87 @@ const prompt = [
issueBody || "(empty)",
].join("\n")
const response = await fetch(`${anthropicBaseUrl.replace(/\/$/, "")}/v1/messages`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": anthropicToken,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: anthropicModel,
max_tokens: 4096,
temperature: 0.2,
messages: [
{
role: "user",
content: prompt,
},
],
}),
})
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
if (!response.ok) {
const errText = await response.text()
console.error(`Anthropic API error: ${response.status} ${errText}`)
function shouldRetry(status) {
return status === 408 || status === 429 || status >= 500
}
async function requestClaude() {
const endpoint = `${anthropicBaseUrl.replace(/\/$/, "")}/v1/messages`
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": anthropicToken,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: anthropicModel,
max_tokens: maxTokens,
temperature: 0.2,
messages: [
{
role: "user",
content: prompt,
},
],
}),
signal: controller.signal,
})
const rawText = await response.text()
if (!response.ok) {
if (attempt < maxRetries && shouldRetry(response.status)) {
const delay = retryBaseMs * 2 ** (attempt - 1)
console.warn(`Anthropic returned ${response.status}, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`)
await sleep(delay)
continue
}
console.error(`Anthropic API error: ${response.status} ${rawText}`)
process.exit(1)
}
try {
return JSON.parse(rawText)
} catch {
console.error("Anthropic API returned non-JSON response")
console.error(rawText)
process.exit(1)
}
} catch (error) {
const isAbort = error instanceof Error && error.name === "AbortError"
if (attempt < maxRetries) {
const delay = retryBaseMs * 2 ** (attempt - 1)
console.warn(
`${isAbort ? "Request timed out" : "Request failed"}, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`,
)
await sleep(delay)
continue
}
console.error(isAbort ? `Anthropic request timeout after ${timeoutMs}ms` : `Anthropic request failed: ${String(error)}`)
process.exit(1)
} finally {
clearTimeout(timer)
}
}
console.error("Anthropic request exhausted retries")
process.exit(1)
}
const data = await response.json()
const data = await requestClaude()
const outputText = Array.isArray(data?.content)
? data.content
.filter((part) => part?.type === "text" && typeof part?.text === "string")
@@ -89,4 +175,4 @@ if (!parsed.patch || typeof parsed.patch !== "string") {
await writeFile("/tmp/issue-autofix.patch", parsed.patch)
await writeFile("/tmp/issue-autofix-summary.txt", String(parsed.summary || ""))
console.log("Generated patch at /tmp/issue-autofix.patch")
console.log(`Generated patch at /tmp/issue-autofix.patch using model ${anthropicModel}`)