mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 21:50:46 +08:00
277 lines
11 KiB
YAML
277 lines
11 KiB
YAML
name: GitHub Content Translator
|
|
run-name: "Translate: ${{ github.event.issue.title || github.event.pull_request.title || github.event.comment.body || github.event.review.body || 'GitHub content' }}"
|
|
|
|
concurrency:
|
|
group: translator-${{ github.event.comment.id || github.event.issue.number || github.event.review.id }}
|
|
cancel-in-progress: true
|
|
|
|
on:
|
|
issues:
|
|
types: [opened]
|
|
issue_comment:
|
|
types: [created, edited]
|
|
pull_request_review:
|
|
types: [submitted, edited]
|
|
pull_request_review_comment:
|
|
types: [created, edited]
|
|
|
|
jobs:
|
|
translate:
|
|
if: |
|
|
(github.event_name == 'issues')
|
|
|| (github.event_name == 'issue_comment' && github.event.sender.type != 'Bot')
|
|
|| (
|
|
(github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment')
|
|
&& github.event.sender.type != 'Bot'
|
|
&& github.event.pull_request.head.repo.fork == false
|
|
)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
issues: write
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- name: Translate and update event content
|
|
uses: actions/github-script@v8
|
|
env:
|
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }}
|
|
ANTHROPIC_MODEL: ${{ vars.ANTHROPIC_MODEL }}
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
script: |
|
|
const ORIGINAL_SUMMARY = '<summary>Original Content</summary>'
|
|
|
|
const stripCodeFence = (value) =>
|
|
value
|
|
.trim()
|
|
.replace(/^```(?:json)?\s*/i, '')
|
|
.replace(/\s*```$/i, '')
|
|
.trim()
|
|
|
|
const parseTranslatorResult = (text, responseMetadata) => {
|
|
const strippedText = stripCodeFence(text)
|
|
|
|
try {
|
|
return JSON.parse(strippedText)
|
|
} catch (error) {
|
|
core.startGroup('Translator response metadata')
|
|
core.info(JSON.stringify(responseMetadata, null, 2))
|
|
core.endGroup()
|
|
|
|
core.startGroup('Translator raw model response')
|
|
core.info(text)
|
|
core.endGroup()
|
|
|
|
if (strippedText !== text) {
|
|
core.startGroup('Translator response after code-fence stripping')
|
|
core.info(strippedText)
|
|
core.endGroup()
|
|
}
|
|
|
|
throw new Error(`Translator API returned invalid JSON: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
const getTranslatedMarker = (kind) => {
|
|
switch (kind) {
|
|
case 'issue':
|
|
return 'This issue was translated automatically.'
|
|
case 'issue_comment':
|
|
return 'This comment was translated automatically.'
|
|
case 'pull_request_review':
|
|
return 'This review was translated automatically.'
|
|
case 'pull_request_review_comment':
|
|
return 'This review comment was translated automatically.'
|
|
default:
|
|
return 'This content was translated automatically.'
|
|
}
|
|
}
|
|
|
|
const buildBody = (kind, translatedBody, originalBody) =>
|
|
[
|
|
`> ${getTranslatedMarker(kind)}`,
|
|
'',
|
|
translatedBody.trim(),
|
|
'',
|
|
'---',
|
|
'<details>',
|
|
ORIGINAL_SUMMARY,
|
|
'',
|
|
originalBody.trim(),
|
|
'</details>'
|
|
].join('\n')
|
|
|
|
const getTarget = () => {
|
|
const payload = context.payload
|
|
|
|
switch (context.eventName) {
|
|
case 'issues':
|
|
return {
|
|
kind: 'issue',
|
|
issueNumber: payload.issue.number,
|
|
title: payload.issue.title || '',
|
|
body: payload.issue.body || ''
|
|
}
|
|
case 'issue_comment':
|
|
return {
|
|
kind: 'issue_comment',
|
|
issueNumber: payload.issue.number,
|
|
commentId: payload.comment.id,
|
|
title: '',
|
|
body: payload.comment.body || ''
|
|
}
|
|
case 'pull_request_review':
|
|
return {
|
|
kind: 'pull_request_review',
|
|
pullNumber: payload.pull_request.number,
|
|
reviewId: payload.review.id,
|
|
title: '',
|
|
body: payload.review.body || ''
|
|
}
|
|
case 'pull_request_review_comment':
|
|
return {
|
|
kind: 'pull_request_review_comment',
|
|
pullNumber: payload.pull_request.number,
|
|
commentId: payload.comment.id,
|
|
title: '',
|
|
body: payload.comment.body || ''
|
|
}
|
|
default:
|
|
throw new Error(`Unsupported event: ${context.eventName}`)
|
|
}
|
|
}
|
|
|
|
const updateTarget = async (target, translatedTitle, translatedBody, originalBody) => {
|
|
const body = buildBody(target.kind, translatedBody, originalBody)
|
|
const { owner, repo } = context.repo
|
|
|
|
switch (target.kind) {
|
|
case 'issue':
|
|
await github.rest.issues.update({
|
|
owner,
|
|
repo,
|
|
issue_number: target.issueNumber,
|
|
title: translatedTitle || target.title,
|
|
body
|
|
})
|
|
return
|
|
case 'issue_comment':
|
|
await github.rest.issues.updateComment({
|
|
owner,
|
|
repo,
|
|
comment_id: target.commentId,
|
|
body
|
|
})
|
|
return
|
|
case 'pull_request_review':
|
|
await github.request('PUT /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}', {
|
|
owner,
|
|
repo,
|
|
pull_number: target.pullNumber,
|
|
review_id: target.reviewId,
|
|
body
|
|
})
|
|
return
|
|
case 'pull_request_review_comment':
|
|
await github.rest.pulls.updateReviewComment({
|
|
owner,
|
|
repo,
|
|
comment_id: target.commentId,
|
|
body
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
const callTranslator = async (target) => {
|
|
const apiKey = process.env.ANTHROPIC_API_KEY
|
|
if (!apiKey) {
|
|
throw new Error('Missing ANTHROPIC_API_KEY')
|
|
}
|
|
|
|
const baseUrl = (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com').replace(/\/$/, '')
|
|
const model = process.env.ANTHROPIC_MODEL
|
|
if (!model) {
|
|
throw new Error('Missing ANTHROPIC_MODEL')
|
|
}
|
|
|
|
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-api-key': apiKey,
|
|
'anthropic-version': '2023-06-01'
|
|
},
|
|
body: JSON.stringify({
|
|
model,
|
|
max_tokens: 4096,
|
|
temperature: 0,
|
|
system: `You translate GitHub issue, issue comment, pull request review, and pull request review comment content into English.
|
|
|
|
Return only compact JSON with this exact shape:
|
|
{
|
|
"should_update": boolean,
|
|
"translated_title": string | null,
|
|
"translated_body": string | null,
|
|
"original_body": string | null
|
|
}
|
|
|
|
Rules:
|
|
- If the content is already fully English, return should_update=false.
|
|
- If the content already follows the automatic translation format and its Original Content matches the current source, return should_update=false.
|
|
- If the content is translated but the original and translated content do not match, return should_update=true and fix it.
|
|
- Translate non-English or partially non-English content to natural English.
|
|
- For issues, translate the title when it is non-English. For all other event kinds, translated_title must be null.
|
|
- Preserve Markdown structure, code blocks, task lists, links, and mentions.
|
|
- If the content quotes an earlier automatic translation, keep only the English quoted content and remove translation markers and Original Content sections from the quote.
|
|
- If the content quotes non-translated content, keep that quote unchanged.
|
|
- If the content is an email reply, remove quoted email history from both translated_body and original_body.
|
|
- original_body must be the source text that corresponds to translated_body, after any required email quote cleanup.`,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: JSON.stringify({
|
|
event: context.eventName,
|
|
target
|
|
})
|
|
}
|
|
]
|
|
})
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Translator API failed with ${response.status}: ${await response.text()}`)
|
|
}
|
|
|
|
const data = await response.json()
|
|
const text = data.content?.find((part) => part.type === 'text')?.text
|
|
if (!text) {
|
|
throw new Error('Translator API returned no text content')
|
|
}
|
|
|
|
return parseTranslatorResult(text, {
|
|
id: data.id,
|
|
model: data.model,
|
|
stop_reason: data.stop_reason,
|
|
stop_sequence: data.stop_sequence,
|
|
usage: data.usage
|
|
})
|
|
}
|
|
|
|
const target = getTarget()
|
|
|
|
const result = await callTranslator(target)
|
|
if (!result.should_update) {
|
|
core.info('Translator reported that no translation update is needed.')
|
|
return
|
|
}
|
|
|
|
if (typeof result.translated_body !== 'string' || typeof result.original_body !== 'string') {
|
|
throw new Error('Translator requested an update without translated_body and original_body')
|
|
}
|
|
|
|
await updateTarget(target, result.translated_title, result.translated_body, result.original_body)
|
|
core.info(`Updated ${target.kind} with English translation.`)
|