From 70e8a13368765777d46f89fac7195c93ad346c9f Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 3 Apr 2026 00:24:11 +0800 Subject: [PATCH] refactor: add remote runtime adapter --- local-agent/server.mjs | 58 +++++++++++++------ .../tasks/[taskId]/complete/route.ts | 18 +++--- .../backends/boss-native-orchestrator.ts | 11 ++++ src/lib/execution/remote-runtime-adapter.ts | 43 ++++++++++++++ tests/remote-runtime-adapter.test.ts | 39 +++++++++++++ 5 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 src/lib/execution/backends/boss-native-orchestrator.ts create mode 100644 src/lib/execution/remote-runtime-adapter.ts create mode 100644 tests/remote-runtime-adapter.test.ts diff --git a/local-agent/server.mjs b/local-agent/server.mjs index 01a4548..c0230de 100755 --- a/local-agent/server.mjs +++ b/local-agent/server.mjs @@ -336,6 +336,24 @@ function parseDispatchExecutionCompletion(rawOutput) { }; } +function buildRemoteExecutionCompletionPayload(task, payload) { + return { + taskId: task.taskId, + status: payload.status === "failed" ? "failed" : "completed", + requestId: payload.requestId, + replyBody: typeof payload.replyBody === "string" ? payload.replyBody.trim() || undefined : undefined, + errorMessage: typeof payload.errorMessage === "string" ? payload.errorMessage.trim() || undefined : undefined, + dispatchExecutionId: + typeof payload.dispatchExecutionId === "string" ? payload.dispatchExecutionId.trim() || undefined : undefined, + targetProjectId: + typeof payload.targetProjectId === "string" ? payload.targetProjectId.trim() || undefined : undefined, + targetThreadId: + typeof payload.targetThreadId === "string" ? payload.targetThreadId.trim() || undefined : undefined, + rawThreadReply: + typeof payload.rawThreadReply === "string" ? payload.rawThreadReply.trim() || undefined : undefined, + }; +} + async function runMasterAgentTask(config, runtime, task) { const outputFile = join(os.tmpdir(), `${task.taskId}.reply.txt`); const stderrChunks = []; @@ -372,15 +390,18 @@ async function runMasterAgentTask(config, runtime, task) { task.taskType === "dispatch_execution" ? parseDispatchExecutionCompletion(replyBody) : null; - const completion = await completeMasterAgentTask(config, runtime, { - taskId: task.taskId, - status: "completed", - replyBody: dispatchExecutionCompletion?.replyBody ?? replyBody, - dispatchExecutionId: task.dispatchExecutionId, - targetProjectId: task.targetProjectId, - targetThreadId: task.targetThreadId, - rawThreadReply: dispatchExecutionCompletion?.rawThreadReply, - }); + const completion = await completeMasterAgentTask( + config, + runtime, + buildRemoteExecutionCompletionPayload(task, { + status: "completed", + replyBody: dispatchExecutionCompletion?.replyBody ?? replyBody, + dispatchExecutionId: task.dispatchExecutionId, + targetProjectId: task.targetProjectId, + targetThreadId: task.targetThreadId, + rawThreadReply: dispatchExecutionCompletion?.rawThreadReply, + }), + ); runtime.activeMasterTask = { taskId: task.taskId, status: completion.ok ? "completed" : "complete_failed", @@ -403,14 +424,17 @@ async function runMasterAgentTask(config, runtime, task) { completedAt: new Date().toISOString(), detail, }; - await completeMasterAgentTask(config, runtime, { - taskId: task.taskId, - status: "failed", - errorMessage: detail, - dispatchExecutionId: task.dispatchExecutionId, - targetProjectId: task.targetProjectId, - targetThreadId: task.targetThreadId, - }).catch(() => null); + await completeMasterAgentTask( + config, + runtime, + buildRemoteExecutionCompletionPayload(task, { + status: "failed", + errorMessage: detail, + dispatchExecutionId: task.dispatchExecutionId, + targetProjectId: task.targetProjectId, + targetThreadId: task.targetThreadId, + }), + ).catch(() => null); await postAppLog(config, runtime, { projectId: "master-agent", level: "error", diff --git a/src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts b/src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts index 1e42502..36549de 100644 --- a/src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts +++ b/src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth"; import { completeMasterAgentTask } from "@/lib/boss-data"; +import { normalizeRemoteExecutionResult } from "@/lib/execution/remote-runtime-adapter"; export async function POST( request: NextRequest, @@ -30,17 +31,18 @@ export async function POST( const { taskId } = await context.params; try { + const normalized = normalizeRemoteExecutionResult(body); const task = await completeMasterAgentTask({ taskId, deviceId: body.deviceId.trim(), - status: body.status === "failed" ? "failed" : "completed", - replyBody: body.replyBody, - errorMessage: body.errorMessage, - requestId: body.requestId, - dispatchExecutionId: body.dispatchExecutionId, - targetProjectId: body.targetProjectId, - targetThreadId: body.targetThreadId, - rawThreadReply: body.rawThreadReply, + status: normalized.status, + replyBody: normalized.replyBody, + errorMessage: normalized.errorMessage, + requestId: normalized.requestId, + dispatchExecutionId: normalized.dispatchExecutionId, + targetProjectId: normalized.targetProjectId, + targetThreadId: normalized.targetThreadId, + rawThreadReply: normalized.rawThreadReply, }); return NextResponse.json({ ok: true, task }); } catch (error) { diff --git a/src/lib/execution/backends/boss-native-orchestrator.ts b/src/lib/execution/backends/boss-native-orchestrator.ts new file mode 100644 index 0000000..5722531 --- /dev/null +++ b/src/lib/execution/backends/boss-native-orchestrator.ts @@ -0,0 +1,11 @@ +import type { OrchestrationBackend } from "@/lib/execution/orchestration-backend"; + +export const BOSS_NATIVE_ORCHESTRATOR: OrchestrationBackend = { + backendId: "boss-native-orchestrator", + async describe() { + return { + backendId: "boss-native-orchestrator", + label: "Boss Native Orchestrator", + }; + }, +}; diff --git a/src/lib/execution/remote-runtime-adapter.ts b/src/lib/execution/remote-runtime-adapter.ts new file mode 100644 index 0000000..7197740 --- /dev/null +++ b/src/lib/execution/remote-runtime-adapter.ts @@ -0,0 +1,43 @@ +export interface RemoteExecutionResultInput { + status: "completed" | "failed"; + dispatchExecutionId?: string; + targetProjectId?: string; + targetThreadId?: string; + rawThreadReply?: string; + replyBody?: string; + errorMessage?: string; + requestId?: string; +} + +export interface NormalizedRemoteExecutionResult { + status: "completed" | "failed"; + dispatchExecutionId?: string; + targetProjectId?: string; + targetThreadId?: string; + rawThreadReply?: string; + replyBody?: string; + errorMessage?: string; + requestId?: string; +} + +function trimToDefined(value: string | undefined) { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +export function normalizeRemoteExecutionResult( + input: RemoteExecutionResultInput, +): NormalizedRemoteExecutionResult { + return { + status: input.status === "failed" ? "failed" : "completed", + dispatchExecutionId: trimToDefined(input.dispatchExecutionId), + targetProjectId: trimToDefined(input.targetProjectId), + targetThreadId: trimToDefined(input.targetThreadId), + rawThreadReply: trimToDefined(input.rawThreadReply), + replyBody: trimToDefined(input.replyBody), + errorMessage: trimToDefined(input.errorMessage), + requestId: trimToDefined(input.requestId), + }; +} + +export const normalizeRemoteExecutionResultForTesting = normalizeRemoteExecutionResult; diff --git a/tests/remote-runtime-adapter.test.ts b/tests/remote-runtime-adapter.test.ts new file mode 100644 index 0000000..0c3ae0d --- /dev/null +++ b/tests/remote-runtime-adapter.test.ts @@ -0,0 +1,39 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { normalizeRemoteExecutionResultForTesting } from "../src/lib/execution/remote-runtime-adapter.ts"; + +test("RemoteRuntimeAdapter 会把 local-agent 回写标准化成统一结果", () => { + const normalized = normalizeRemoteExecutionResultForTesting({ + status: "completed", + dispatchExecutionId: "dx-1", + targetProjectId: "project-1", + targetThreadId: "thread-1", + rawThreadReply: " 链路正常 ", + replyBody: " 主 Agent 汇总:链路正常 ", + }); + + assert.equal(normalized.status, "completed"); + assert.equal(normalized.dispatchExecutionId, "dx-1"); + assert.equal(normalized.targetProjectId, "project-1"); + assert.equal(normalized.targetThreadId, "thread-1"); + assert.equal(normalized.rawThreadReply, "链路正常"); + assert.equal(normalized.replyBody, "主 Agent 汇总:链路正常"); +}); + +test("RemoteRuntimeAdapter 会忽略空白字段并保留失败状态", () => { + const normalized = normalizeRemoteExecutionResultForTesting({ + status: "failed", + dispatchExecutionId: " ", + targetProjectId: " project-2 ", + targetThreadId: "", + rawThreadReply: " ", + errorMessage: " MODEL_CALL_FAILED ", + }); + + assert.equal(normalized.status, "failed"); + assert.equal(normalized.dispatchExecutionId, undefined); + assert.equal(normalized.targetProjectId, "project-2"); + assert.equal(normalized.targetThreadId, undefined); + assert.equal(normalized.rawThreadReply, undefined); + assert.equal(normalized.errorMessage, "MODEL_CALL_FAILED"); +});