diff --git a/internal/cloud/access-control.test.ts b/internal/cloud/access-control.test.ts index de5b3cdd..e3ea7c7e 100644 --- a/internal/cloud/access-control.test.ts +++ b/internal/cloud/access-control.test.ts @@ -1400,8 +1400,13 @@ test("cloud api dispatches authorized device jobs to the internal device-pod exe traceId: "trc_executor_refresh", operationId: "op_executor_refresh", job: { id: request.url.split("/").at(-2), devicePodId: "device-pod-71-freq", status: "completed", intent: "workspace.ls" }, - output: { text: "executor output", bytes: 15, truncation: { maxBytes: 12000, truncated: false } }, - text: "executor output" + output: { + executor: { status: "completed" }, + output: { dispatch: { exitCode: 0, stdoutBytes: 15 } }, + text: "executor output", + bytes: 15, + truncation: { maxBytes: 12000, truncated: false } + } })); return; } @@ -1468,6 +1473,7 @@ test("cloud api dispatches authorized device jobs to the internal device-pod exe assert.equal(output.body.blocker, null); assert.equal(output.body.freshness.stale, false); assert.equal(output.body.output.text, "executor output"); + assert.deepEqual(output.body.output.output.dispatch, { exitCode: 0, stdoutBytes: 15 }); assert.equal(output.body.truncation.truncated, false); assert.ok(executorRequests.some((item) => item.method === "GET" && item.url.endsWith(`/jobs/${runningJob.body.job.id}/output`))); diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index 6d9ff12f..5a2ff642 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -1765,6 +1765,7 @@ async function jsonBody(request) { function requiredText(value, field) { const text = textOr(value, ""); if (!text) throw Object.assign(new Error(`${field} is required`), { statusCode: 400, code: "invalid_params" }); return text; } function textOr(value, fallback) { const text = String(value ?? "").trim(); return text || fallback; } +function firstString(...values) { return values.find((value) => typeof value === "string") ?? ""; } function normalizeObject(value) { return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {}; } function assertDevicePodProfileSafe(profile, field = "profile") { const findings = []; @@ -1844,7 +1845,17 @@ function deviceJobBlocked(code, summary, retryable) { return { code, layer: "dev function devicePodExecutorBlocker(summary) { return { code: "device_pod_executor_unavailable", layer: "device-pod", retryable: true, summary, userMessage: "Device Pod 已授权,但内部执行服务当前不可用。" }; } function gatewayDispatchBlocker(summary) { return { code: "gateway_dispatch_unavailable", layer: "device-pod", retryable: true, summary, userMessage: "Device Pod 已授权,但当前没有可用 gateway/device-host-cli 执行通道。" }; } function normalizeDeviceJobStatus(status, httpStatus) { const text = textOr(status, ""); return ["queued", "running", "completed", "failed", "blocked", "canceled"].includes(text) ? text : httpStatus >= 400 ? "blocked" : "running"; } -function executorOutputPayload(body, httpStatus) { return { executor: body ?? {}, output: normalizeObject(body?.output), text: typeof body?.text === "string" ? body.text : typeof body?.output?.text === "string" ? body.output.text : "", httpStatus }; } +function executorOutputPayload(body, httpStatus) { + const output = normalizeObject(body?.output); + const nestedOutput = normalizeObject(output.output); + const text = firstString(body?.text, output.text, nestedOutput.text); + return { + executor: body ?? {}, + output: Object.keys(nestedOutput).length > 0 ? nestedOutput : output, + text, + httpStatus + }; +} function boundedOutput(output, maxBytes = DEVICE_JOB_OUTPUT_MAX_BYTES) { const source = normalizeObject(output); const text = typeof source.text === "string" ? source.text : stableJson(source);