feat: add hapi resume command (#647)

This commit is contained in:
leko
2026-05-20 06:18:42 +08:00
committed by GitHub
parent 79d919675e
commit 197f327590
29 changed files with 1777 additions and 93 deletions
+115 -1
View File
@@ -1,6 +1,18 @@
import axios from 'axios'
import type { AgentState, CreateMachineResponse, CreateSessionResponse, RunnerState, Machine, MachineMetadata, Metadata, Session } from '@/api/types'
import { AgentStateSchema, CreateMachineResponseSchema, CreateSessionResponseSchema, RunnerStateSchema, MachineMetadataSchema, MetadataSchema } from '@/api/types'
import type { LocalResumeTarget, ResumableSession } from '@hapi/protocol'
import {
AgentStateSchema,
CreateMachineResponseSchema,
CreateSessionResponseSchema,
GetSessionResponseSchema,
LocalHandoffResponseSchema,
LocalResumeTargetResponseSchema,
RunnerStateSchema,
MachineMetadataSchema,
MetadataSchema,
ResumableSessionsResponseSchema
} from '@/api/types'
import { configuration } from '@/configuration'
import { getAuthToken } from '@/api/auth'
import { apiValidationError } from '@/utils/errorUtils'
@@ -15,6 +27,13 @@ export class ApiClient {
private constructor(private readonly token: string) { }
private authHeaders(): Record<string, string> {
return buildHubRequestHeaders({
Authorization: `Bearer ${this.token}`,
'Content-Type': 'application/json'
})
}
async getOrCreateSession(opts: {
tag: string
metadata: Metadata
@@ -84,6 +103,55 @@ export class ApiClient {
}
}
async getSession(sessionId: string): Promise<Session> {
const response = await axios.get(
`${configuration.apiUrl}/cli/sessions/${encodeURIComponent(sessionId)}`,
{
headers: this.authHeaders(),
timeout: 60_000
}
)
const parsed = GetSessionResponseSchema.safeParse(response.data)
if (!parsed.success) {
throw apiValidationError('Invalid /cli/sessions/:id response', response)
}
const raw = parsed.data.session
const metadata = (() => {
if (raw.metadata == null) return null
const parsedMetadata = MetadataSchema.safeParse(raw.metadata)
return parsedMetadata.success ? parsedMetadata.data : null
})()
const agentState = (() => {
if (raw.agentState == null) return null
const parsedAgentState = AgentStateSchema.safeParse(raw.agentState)
return parsedAgentState.success ? parsedAgentState.data : null
})()
return {
id: raw.id,
namespace: raw.namespace,
seq: raw.seq,
createdAt: raw.createdAt,
updatedAt: raw.updatedAt,
active: raw.active,
activeAt: raw.activeAt,
metadata,
metadataVersion: raw.metadataVersion,
agentState,
agentStateVersion: raw.agentStateVersion,
thinking: raw.thinking,
thinkingAt: raw.thinkingAt,
todos: raw.todos,
model: raw.model,
modelReasoningEffort: raw.modelReasoningEffort,
effort: raw.effort,
permissionMode: raw.permissionMode,
collaborationMode: raw.collaborationMode
}
}
async getOrCreateMachine(opts: {
machineId: string
metadata: MachineMetadata
@@ -138,6 +206,52 @@ export class ApiClient {
}
}
async listResumableSessions(machineId?: string): Promise<ResumableSession[]> {
const qs = machineId ? `?machineId=${encodeURIComponent(machineId)}` : ''
const response = await axios.get(
`${configuration.apiUrl}/cli/sessions/resumable${qs}`,
{
headers: this.authHeaders(),
timeout: 60_000
}
)
const parsed = ResumableSessionsResponseSchema.safeParse(response.data)
if (!parsed.success) {
throw apiValidationError('Invalid /cli/sessions/resumable response', response)
}
return parsed.data.sessions
}
async getLocalResumeTarget(sessionId: string): Promise<LocalResumeTarget> {
const response = await axios.get(
`${configuration.apiUrl}/cli/sessions/${encodeURIComponent(sessionId)}/resume-target`,
{
headers: this.authHeaders(),
timeout: 60_000
}
)
const parsed = LocalResumeTargetResponseSchema.safeParse(response.data)
if (!parsed.success) {
throw apiValidationError('Invalid /cli/sessions/:id/resume-target response', response)
}
return parsed.data.target
}
async handoffSessionToLocal(sessionId: string): Promise<void> {
const response = await axios.post(
`${configuration.apiUrl}/cli/sessions/${encodeURIComponent(sessionId)}/handoff-local`,
{},
{
headers: this.authHeaders(),
timeout: 60_000
}
)
const parsed = LocalHandoffResponseSchema.safeParse(response.data)
if (!parsed.success || !parsed.data.ok) {
throw apiValidationError('Invalid /cli/sessions/:id/handoff-local response', response)
}
}
sessionSyncClient(session: Session): ApiSessionClient {
return new ApiSessionClient(this.token, session)
}
+14
View File
@@ -6,6 +6,11 @@ import {
PermissionModeSchema,
TodosSchema
} from '@hapi/protocol/schemas'
import {
LocalHandoffResponseSchema,
LocalResumeTargetResponseSchema,
ResumableSessionsResponseSchema
} from '@hapi/protocol'
import type { CodexCollaborationMode, PermissionMode } from '@hapi/protocol/types'
import { z } from 'zod'
import { UsageSchema } from '@/claude/types'
@@ -142,6 +147,15 @@ export const CreateMachineResponseSchema = z.object({
export type CreateMachineResponse = z.infer<typeof CreateMachineResponseSchema>
export const GetSessionResponseSchema = CreateSessionResponseSchema
export type GetSessionResponse = z.infer<typeof GetSessionResponseSchema>
export {
LocalHandoffResponseSchema,
LocalResumeTargetResponseSchema,
ResumableSessionsResponseSchema
}
export const MessageMetaSchema = z.object({
sentFrom: z.string().optional(),
fallbackModel: z.string().nullable().optional(),