import { httpClient } from '@/lib/http-client'
import {
BackportApplyRowRequest,
BackportAttemptListResponse,
BackportAsyncRunResponse,
BackportBrowseResponse,
BackportCommitMessagePreview,
BackportCommitMessagePreviewRequest,
BackportConfig,
BackportConfigUpdateResponse,
BackportContinueReportRequest,
BackportExecutionListResponse,
BackportExecuteRequest,
BackportGenerateReportRequest,
BackportLoadGitLogRequest,
BackportLoadPatchPreviewRequest,
BackportLoadGitShowRequest,
BackportLoadReportRequest,
BackportManualPatchRequest,
BackportPatchPreviewResponse,
BackportRecentRepositoriesResponse,
BackportRepositoryInfo,
BackportRepositoryPrepareResponse,
BackportRepositoryRole,
BackportRecheckConflictRequest,
BackportRuntimeStatus,
BackportRunAllControl,
BackportRunAllLifecycle,
BackportRunAllRequest,
BackportRunProgress,
BackportRunListResponse,
BackportRunResponse,
BackportToolSnapshot,
BackportTryResolveRequest,
} from '@/lib/backport-types'
type BackportAction =
| 'run_all'
| 'generate_report'
| 'load_report'
| 'continue_report'
| 'recheck_conflict'
| 'load_git_log'
| 'load_git_show'
| 'load_patch_preview'
| 'preview_commit_message'
| 'execute_selected'
| 'apply_row'
| 'try_resolve'
| 'check_manual_patch'
| 'apply_manual_patch'
type BackportRunRequest = {
action: BackportAction
payload: Record<string, unknown>
}
function parseJsonObject(text: string): Record<string, unknown> {
try {
const value = JSON.parse(text)
return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
} catch {
return {}
}
}
class BackportService {
public async getConfig(): Promise<BackportConfig> {
return httpClient.get<BackportConfig>('/backport/config')
}
public async updateConfig(payload: BackportConfig): Promise<BackportConfigUpdateResponse> {
return httpClient.put<BackportConfigUpdateResponse>('/backport/config', payload)
}
public async getRuntimeStatus(payload: BackportConfig): Promise<BackportRuntimeStatus> {
return httpClient.post<BackportRuntimeStatus>('/backport/runtime-status', payload)
}
public async browsePath(path?: string): Promise<BackportBrowseResponse> {
const query = path ? `?path=${encodeURIComponent(path)}` : ''
return httpClient.get<BackportBrowseResponse>(`/backport/browse${query}`)
}
public async getRecentRepositories(): Promise<BackportRecentRepositoriesResponse> {
return httpClient.get<BackportRecentRepositoriesResponse>('/backport/repositories/recent')
}
public async prepareRepository(request: {
role: BackportRepositoryRole
input: string
preferredBranch?: string
}): Promise<BackportRepositoryPrepareResponse> {
return httpClient.post<BackportRepositoryPrepareResponse>('/backport/repositories/prepare', {
role: request.role,
input: request.input,
preferred_branch: request.preferredBranch || '',
})
}
public async getRepositoryPrepareTask(
taskId: string
): Promise<BackportRepositoryPrepareResponse> {
return httpClient.get<BackportRepositoryPrepareResponse>(
`/backport/repositories/prepare/${encodeURIComponent(taskId)}`
)
}
public async refreshRepository(request: {
role: BackportRepositoryRole
localPath: string
sourceUrl?: string
selectedBranch?: string
}): Promise<BackportRepositoryInfo> {
return httpClient.post<BackportRepositoryInfo>('/backport/repositories/refresh', {
role: request.role,
local_path: request.localPath,
source_url: request.sourceUrl || '',
selected_branch: request.selectedBranch || '',
})
}
public async pauseRun(runId: string): Promise<BackportAsyncRunResponse> {
return httpClient.post<BackportAsyncRunResponse>(
`/backport/runs/${encodeURIComponent(runId)}/pause`,
{},
{ timeout: 30000 }
)
}
public async getRun(runId: string): Promise<BackportAsyncRunResponse> {
return httpClient.get<BackportAsyncRunResponse>(`/backport/runs/${encodeURIComponent(runId)}`, {
timeout: 30000,
})
}
public async listRuns(): Promise<BackportRunListResponse> {
return httpClient.get<BackportRunListResponse>('/backport/tasks', { timeout: 30000 })
}
public async listCaseAttempts(
runId: string,
rowKey: string
): Promise<BackportAttemptListResponse> {
return httpClient.get<BackportAttemptListResponse>(
`/backport/runs/${encodeURIComponent(runId)}/cases/${encodeURIComponent(rowKey)}/attempts`,
{ timeout: 30000 }
)
}
public async listExecutions(runId: string): Promise<BackportExecutionListResponse> {
return httpClient.get<BackportExecutionListResponse>(
`/backport/tasks/${encodeURIComponent(runId)}/runs`,
{ timeout: 30000 }
)
}
public async loadPatchPreview(
request: BackportLoadPatchPreviewRequest,
onEvent?: (event: any) => void
): Promise<BackportPatchPreviewResponse> {
const response = await this.runAction(
{
action: 'load_patch_preview',
payload: {
base_report_path: request.baseReportPath,
working_report_path: request.workingReportPath,
row: request.row,
patch_kind: request.kind,
},
},
onEvent
)
const patch = response.parsedResult?.patch
if (!patch) {
throw new Error('未返回 patch 预览内容')
}
return patch
}
public async previewCommitMessage(
request: BackportCommitMessagePreviewRequest,
onEvent?: (event: any) => void
): Promise<BackportCommitMessagePreview> {
const response = await this.runAction(
{
action: 'preview_commit_message',
payload: {
config: request.config,
base_report_path: request.baseReportPath,
working_report_path: request.workingReportPath,
row: request.row,
commit_message_template: request.commitMessageTemplate,
},
},
onEvent
)
const preview = response.parsedResult?.commit_message
if (!preview) {
throw new Error('未返回 commit message 预览内容')
}
return preview
}
public async generateReport(
request: BackportGenerateReportRequest,
onEvent?: (event: any) => void,
lifecycle?: BackportRunAllLifecycle
): Promise<BackportRunResponse> {
onEvent?.({ type: 'message.started', payload: {} })
const runRequest: BackportRunRequest = {
action: 'generate_report',
payload: {
config: request.config,
excel_path: request.excelPath,
run_id: request.runId,
},
}
const created = await httpClient.post<BackportAsyncRunResponse>('/backport/runs', runRequest, {
timeout: 30000,
})
lifecycle?.onRunCreated?.({
runId: created.run_id,
pause: () => this.pauseRun(created.run_id),
})
lifecycle?.onRunUpdated?.(created)
let current = created
while (current.status === 'running') {
await new Promise(resolve => setTimeout(resolve, 15000))
current = await this.getRun(created.run_id)
lifecycle?.onRunUpdated?.(current)
}
if (current.status === 'failed') {
throw new Error(current.error || '生成配置与报告失败')
}
if (!current.result && (current.status === 'paused' || current.status === 'interrupted')) {
const reportPath = current.progress?.current_report_path
current = {
...current,
result: {
agentId: '',
agentName: '',
sessionId: '',
assistantText: current.error || '任务已暂停,当前进度已保存',
parsedResult: {
operation: current.action,
status: 'success',
stage: 'paused',
summary: current.error || '任务已暂停,当前进度已保存',
artifacts: reportPath
? {
report_path: reportPath,
base_report_path: reportPath,
}
: undefined,
},
toolSnapshots: [],
},
}
}
if (!current.result) {
throw new Error('生成配置与报告未返回结果')
}
this.emitSyntheticToolEvents(current.result.toolSnapshots, onEvent)
onEvent?.({
type: 'message.completed',
payload: {
text: current.result.assistantText,
},
})
return current.result
}
public async loadReport(request: BackportLoadReportRequest): Promise<BackportRunResponse> {
return this.runAction({
action: 'load_report',
payload: {
config: request.config,
base_report_path: request.baseReportPath,
},
})
}
public async runAll(
request: BackportRunAllRequest,
onEvent?: (event: any) => void,
onProgress?: (progress: BackportRunProgress) => void,
lifecycle?: BackportRunAllLifecycle
): Promise<BackportRunResponse> {
onEvent?.({ type: 'message.started', payload: {} })
const runRequest: BackportRunRequest = {
action: 'run_all',
payload: {
config: request.config,
excel_path: request.excelPath,
base_report_path: request.baseReportPath,
working_report_path: request.workingReportPath,
run_id: request.runId,
},
}
const created = await httpClient.post<BackportAsyncRunResponse>('/backport/runs', runRequest, {
timeout: 30000,
})
const control: BackportRunAllControl = {
runId: created.run_id,
pause: () => this.pauseRun(created.run_id),
}
lifecycle?.onRunCreated?.(control)
lifecycle?.onRunUpdated?.(created)
let current = created
let lastProgressText = ''
if (current.progress) {
lastProgressText = JSON.stringify(current.progress)
onProgress?.(current.progress)
}
while (current.status === 'running') {
await new Promise(resolve => setTimeout(resolve, 2000))
current = await this.getRun(created.run_id)
lifecycle?.onRunUpdated?.(current)
if (current.progress) {
const progressText = JSON.stringify(current.progress)
if (progressText !== lastProgressText) {
lastProgressText = progressText
onProgress?.(current.progress)
}
}
}
if (current.progress) {
onProgress?.(current.progress)
}
lifecycle?.onRunUpdated?.(current)
if (current.status === 'failed') {
throw new Error(current.error || '一键运行失败')
}
if (!current.result && (current.status === 'paused' || current.status === 'interrupted')) {
const reportPath = current.progress?.current_report_path
current = {
...current,
result: {
agentId: '',
agentName: '',
sessionId: '',
assistantText: current.error || '任务已暂停,当前进度已保存',
parsedResult: {
operation: current.action,
status: 'success',
stage: 'paused',
summary: current.error || '任务已暂停,当前进度已保存',
artifacts: reportPath
? {
report_path: reportPath,
base_report_path: reportPath,
}
: undefined,
},
toolSnapshots: [],
},
}
}
if (!current.result) {
throw new Error('一键运行未返回结果')
}
this.emitSyntheticToolEvents(current.result.toolSnapshots, onEvent)
onEvent?.({
type: 'message.completed',
payload: {
text: current.result.assistantText,
},
})
return current.result
}
public async resumeRun(
runId: string,
onProgress?: (progress: BackportRunProgress) => void,
lifecycle?: BackportRunAllLifecycle
): Promise<BackportAsyncRunResponse> {
let current = await this.getRun(runId)
lifecycle?.onRunUpdated?.(current)
let lastProgressText = ''
if (current.progress) {
lastProgressText = JSON.stringify(current.progress)
onProgress?.(current.progress)
}
while (current.status === 'running') {
await new Promise(resolve => setTimeout(resolve, 2000))
current = await this.getRun(runId)
lifecycle?.onRunUpdated?.(current)
if (current.progress) {
const progressText = JSON.stringify(current.progress)
if (progressText !== lastProgressText) {
lastProgressText = progressText
onProgress?.(current.progress)
}
}
}
return current
}
public async loadGitLog(
request: BackportLoadGitLogRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'load_git_log',
payload: {
config: request.config,
},
},
onEvent
)
}
public async continueReport(
request: BackportContinueReportRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'continue_report',
payload: {
config: request.config,
base_report_path: request.baseReportPath,
},
},
onEvent
)
}
public async recheckConflict(
request: BackportRecheckConflictRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'recheck_conflict',
payload: {
config: request.config,
base_report_path: request.baseReportPath,
working_report_path: request.workingReportPath,
row: request.row,
},
},
onEvent
)
}
public async loadGitShow(
request: BackportLoadGitShowRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'load_git_show',
payload: {
config: request.config,
revision: request.revision,
},
},
onEvent
)
}
public async executeSelected(
request: BackportExecuteRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'execute_selected',
payload: {
config: request.config,
base_report_path: request.baseReportPath,
working_report_path: request.workingReportPath,
selected_commits: request.selectedCommits,
save_source: request.source,
},
},
onEvent
)
}
public async applyRow(
request: BackportApplyRowRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'apply_row',
payload: {
config: request.config,
base_report_path: request.baseReportPath,
working_report_path: request.workingReportPath,
row: request.row,
},
},
onEvent
)
}
public async tryResolve(
request: BackportTryResolveRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'try_resolve',
payload: {
config: request.config,
base_report_path: request.baseReportPath,
working_report_path: request.workingReportPath,
row: request.row,
},
},
onEvent
)
}
public async checkManualPatch(
request: BackportManualPatchRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'check_manual_patch',
payload: {
config: request.config,
patch_text: request.patchText,
},
},
onEvent
)
}
public async applyManualPatch(
request: BackportManualPatchRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
return this.runAction(
{
action: 'apply_manual_patch',
payload: {
config: request.config,
patch_text: request.patchText,
},
},
onEvent
)
}
private async runAction(
request: BackportRunRequest,
onEvent?: (event: any) => void
): Promise<BackportRunResponse> {
onEvent?.({ type: 'message.started', payload: {} })
const response = await httpClient.post<BackportRunResponse>('/backport/run', request, {
timeout: 600000,
})
this.emitSyntheticToolEvents(response.toolSnapshots, onEvent)
onEvent?.({
type: 'message.completed',
payload: {
text: response.assistantText,
},
})
return response
}
private emitSyntheticToolEvents(
toolSnapshots: BackportToolSnapshot[],
onEvent?: (event: any) => void
): void {
if (!onEvent) return
for (const snapshot of toolSnapshots) {
const parsedArguments = parseJsonObject(snapshot.arguments_text)
onEvent({
type: 'tool.call.started',
payload: {
tool_name: snapshot.tool_name,
arguments: parsedArguments,
},
})
onEvent({
type: 'tool.call.response',
payload: {
name: snapshot.tool_name,
content: snapshot.response_text,
is_error: snapshot.is_error,
},
})
}
}
}
export const backportService = new BackportService()