initial platform gateway service

This commit is contained in:
2026-07-08 14:44:20 +09:00
commit 05d0fd80f0
21 changed files with 1770 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
import { config } from '../config'
import type { Platform } from '../types'
export class AgentClient {
async health(platform: Platform): Promise<{ ok: boolean; message?: string }> {
if (!platform.agentUrl) return { ok: false, message: 'platform_agent_url_missing' }
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), Math.min(config.agentRequestTimeoutMs, 3000))
try {
const response = await fetch(`${platform.agentUrl.replace(/\/+$/, '')}/health`, {
headers: this.agentHeaders(),
signal: controller.signal
})
if (!response.ok) return { ok: false, message: `agent_health_failed:${response.status}` }
const payload = await response.json().catch(() => ({ ok: true }))
return { ok: payload?.ok !== false }
} catch (error) {
return { ok: false, message: error instanceof Error ? error.message : String(error) }
} finally {
clearTimeout(timeout)
}
}
async visualizationStart(platform: Platform, input: {
stack?: string;
rosDomainId?: string;
profile?: unknown;
}): Promise<{ ok: boolean; running: boolean; stack: string | null; message?: string }> {
return this.agentJson(platform, '/visualization/start', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(input),
});
}
async visualizationStop(platform: Platform, input: {
stack?: string;
profile?: unknown;
} = {}): Promise<{ ok: boolean; running: boolean; stack: string | null; message?: string }> {
return this.agentJson(platform, '/visualization/stop', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(input),
});
}
async visualizationStatus(platform: Platform): Promise<{ ok: boolean; running: boolean; stack: string | null; message?: string }> {
return this.agentJson(platform, '/visualization/status');
}
async moduleStatuses(platform: Platform): Promise<unknown> {
return this.agentJson(platform, '/api/modules/docker-statuses');
}
async moduleStart(platform: Platform, input: { folderName?: string; rosDomainId?: string }): Promise<unknown> {
return this.agentJson(platform, '/api/modules/start', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(input),
});
}
async moduleStop(platform: Platform, input: { containerKey?: string }): Promise<unknown> {
return this.agentJson(platform, '/api/modules/stop', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(input),
});
}
async syncAuthorizedKeys(platform: Platform, publicKeys: string[], sshUser?: string | null): Promise<unknown> {
return this.agentJson(platform, '/api/ssh/authorized-keys', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ publicKeys, sshUser }),
});
}
async runCommand(platform: Platform, command: string, args: string[]): Promise<{ commandId: string; accepted: boolean }> {
if (!platform.agentUrl) return { commandId: crypto.randomUUID(), accepted: false }
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), config.agentRequestTimeoutMs)
try {
const response = await fetch(`${platform.agentUrl}/commands`, {
method: 'POST',
headers: this.agentHeaders({ 'content-type': 'application/json' }),
body: JSON.stringify({ command, args }),
signal: controller.signal
})
if (!response.ok) throw new Error(`agent_command_failed:${response.status}`)
const payload = await response.json().catch(() => ({}))
return { commandId: String(payload.commandId ?? crypto.randomUUID()), accepted: true }
} catch {
return { commandId: crypto.randomUUID(), accepted: false }
} finally {
clearTimeout(timeout)
}
}
async emergencyStop(platform: Platform): Promise<{ accepted: boolean }> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), config.agentRequestTimeoutMs)
try {
const response = await fetch(`${platform.agentUrl}/emergency-stop`, {
method: 'POST',
headers: this.agentHeaders(),
signal: controller.signal
})
return { accepted: response.ok }
} catch {
return { accepted: false }
} finally {
clearTimeout(timeout)
}
}
private async agentJson<T>(platform: Platform, path: string, init?: RequestInit): Promise<T> {
if (!platform.agentUrl) throw new Error('platform_agent_url_missing')
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), config.agentRequestTimeoutMs)
try {
const response = await fetch(`${platform.agentUrl.replace(/\/+$/, '')}${path}`, {
...init,
headers: this.agentHeaders(init?.headers),
signal: controller.signal,
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(String(payload?.message ?? payload?.error ?? `agent_request_failed:${response.status}`))
return payload as T
} finally {
clearTimeout(timeout)
}
}
private agentHeaders(headers?: HeadersInit): HeadersInit {
const next = new Headers(headers)
if (config.agentGatewayToken) {
next.set('x-agent-gateway-token', config.agentGatewayToken)
}
return next
}
}
export const agentClient = new AgentClient()