diff --git a/internal/cloud/code-agent-chat.mjs b/internal/cloud/code-agent-chat.mjs index b9b5426e..cc67bbf2 100644 --- a/internal/cloud/code-agent-chat.mjs +++ b/internal/cloud/code-agent-chat.mjs @@ -2582,25 +2582,25 @@ function m3IoApiBaseUrlMissingBlocker({ route = HWLAB_M3_IO_API_ROUTE } = {}) { function m3IoSkillReply(skillResult) { const lines = [ - `M3 IO Skill CLI result: status=${skillResult.status}; accepted=${skillResult.accepted}; route=${skillResult.route}; traceId=${skillResult.traceId}; operationId=${skillResult.operationId ?? "null"}.` + `M3 IO Skill CLI result: 已通过 Code Agent -> Skill CLI -> HWLAB API ${skillResult.route} 处理请求;status=${skillResult.status}; accepted=${skillResult.accepted}; traceId=${skillResult.traceId}; operationId=${skillResult.operationId ?? "null"}。` ]; if (skillResult.audit?.summary) { - lines.push(`Audit: ${skillResult.audit.summary}.`); + lines.push(`Audit: ${skillResult.audit.summary}(audit 摘要)。`); } if (skillResult.evidence?.summary) { - lines.push(`Evidence: ${skillResult.evidence.summary}.`); + lines.push(`Evidence: ${skillResult.evidence.summary}(evidence 摘要)。`); } if (skillResult.blocker) { - lines.push(`Blocker: ${skillResult.blocker.code}; ${skillResult.blocker.zh ?? skillResult.blocker.message}.`); + lines.push(`Blocker: ${skillResult.blocker.code}; ${skillResult.blocker.zh ?? skillResult.blocker.message}(blocker)。`); } if (skillResult.action === "do.write") { - lines.push(`DO1 requested value=${String(skillResult.command?.value)}; DI1 readback=${String(skillResult.result?.targetReadback?.value ?? "unknown")}.`); + lines.push(`DO1 写入请求:res_boxsimu_1:DO1=${String(skillResult.command?.value)};DI1 readback:res_boxsimu_2:DI1=${String(skillResult.result?.targetReadback?.value ?? "unknown")}。`); } else if (skillResult.action === "di.read") { - lines.push(`DI1 value=${String(skillResult.result?.value ?? "unknown")}.`); + lines.push(`DI1 读取结果:res_boxsimu_2:DI1=${String(skillResult.result?.value ?? "unknown")}。`); } else if (skillResult.readonly === true || skillResult.route === HWLAB_M3_STATUS_API_ROUTE) { - lines.push(`M3 status readback DI1=${String(skillResult.readback?.value ?? "unknown")}; readiness=${skillResult.readiness?.status ?? "unknown"}.`); + lines.push(`M3 status readback:res_boxsimu_2:DI1=${String(skillResult.readback?.value ?? "unknown")};readiness=${skillResult.readiness?.status ?? "unknown"}。`); } - lines.push(`Boundary: Code Agent -> Skill CLI -> HWLAB API ${skillResult.route}; no direct gateway/box/patch-panel call; no OpenAI fallback used. This is source/artifact control capability, not DEV-LIVE verification.`); + lines.push(`Boundary/边界:本次只走 Code Agent -> Skill CLI -> HWLAB API ${skillResult.route};没有直连 gateway/box/patch-panel,也没有使用 OpenAI fallback 冒充 toolCall。这是受控操作结果,不是 M3 DEV-LIVE 验收结论。`); return boundToolOutput(lines.join("\n"), READONLY_TOOL_OUTPUT_LIMIT).text; } diff --git a/internal/cloud/server.test.mjs b/internal/cloud/server.test.mjs index d89a97ad..0d0fef4d 100644 --- a/internal/cloud/server.test.mjs +++ b/internal/cloud/server.test.mjs @@ -2360,7 +2360,7 @@ test("cloud api /v1/agent/chat routes M3 IO through Skill CLI to /v1/m3/io only" }, body: JSON.stringify({ conversationId: "cnv_server-test-m3-skill", - message: "请通过 M3 DO1 写入 false,并回读 DI1" + message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 false,然后读取 res_boxsimu_2 的 DI1。" }) }); assert.equal(response.status, 200); @@ -2381,6 +2381,9 @@ test("cloud api /v1/agent/chat routes M3 IO through Skill CLI to /v1/m3/io only" assert.equal(payload.providerTrace.fallbackUsed, false); assert.equal(payload.providerTrace.method, "POST"); assert.equal(payload.providerTrace.readback.value, false); + assert.match(payload.reply.content, /HWLAB API \/v1\/m3\/io/u); + assert.match(payload.reply.content, /res_boxsimu_1:DO1=false/u); + assert.match(payload.reply.content, /res_boxsimu_2:DI1=false/u); assert.equal(payload.runner.safety.directGatewayCallsAllowed, false); assert.equal(payload.runner.safety.directBoxSimuCallsAllowed, false); assert.equal(payload.runner.safety.directPatchPanelCallsAllowed, false); @@ -2478,6 +2481,9 @@ test("cloud api /v1/agent/chat routes Codex stdio M3 request through in-process assert.equal(payload.runnerTrace.route, "/v1/m3/io"); assert.equal(payload.providerTrace.codexStdio, true); assert.equal(payload.providerTrace.fallbackUsed, false); + assert.match(payload.reply.content, /HWLAB API \/v1\/m3\/io/u); + assert.match(payload.reply.content, /res_boxsimu_1:DO1=true/u); + assert.match(payload.reply.content, /res_boxsimu_2:DI1=true/u); assert.ok(payload.runner.toolPolicy.allowed.includes("POST /v1/m3/io")); assert.deepEqual(gatewayCalls.map((call) => new URL(call.url).pathname), [ "/status", diff --git a/scripts/code-agent-chat-smoke.mjs b/scripts/code-agent-chat-smoke.mjs index 6c869051..dd67cd91 100644 --- a/scripts/code-agent-chat-smoke.mjs +++ b/scripts/code-agent-chat-smoke.mjs @@ -510,7 +510,7 @@ async function runLocalContractSmoke() { { conversationId: "cnv_code-agent-chat-m3-skill", traceId: "trc_code-agent-chat-m3-skill", - message: "请通过 M3 DO1 写入 true,并回读 DI1" + message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1。" }, { now: () => "2026-05-22T00:04:55.000Z", @@ -583,13 +583,99 @@ async function runLocalContractSmoke() { assert.equal(m3SkillWrite.providerTrace.method, "POST"); assert.equal(m3SkillWrite.providerTrace.readback.value, true); assert.equal(m3SkillWrite.runner.safety.directGatewayCallsAllowed, false); + assert.match(m3SkillWrite.reply.content, /HWLAB API \/v1\/m3\/io/u); + assert.match(m3SkillWrite.reply.content, /res_boxsimu_1:DO1=true/u); + assert.match(m3SkillWrite.reply.content, /res_boxsimu_2:DI1=true/u); assert.equal(m3SkillCalls.length, 1); assert.equal(m3SkillCalls[0].url, "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/m3/io"); + assert.equal(m3SkillCalls[0].request.body.action, "do.write"); + assert.equal(m3SkillCalls[0].request.body.value, true); assert.equal(m3SkillCalls[0].url.includes("gateway-simu"), false); assert.equal(m3SkillCalls[0].url.includes("box-simu"), false); assert.equal(m3SkillCalls[0].url.includes("patch-panel"), false); logOk("M3 Skill CLI routes through HWLAB API only"); + const m3SkillFalseCalls = []; + const m3SkillWriteFalse = await handleCodeAgentChat( + { + conversationId: "cnv_code-agent-chat-m3-skill-false", + traceId: "trc_code-agent-chat-m3-skill-false", + message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 false,然后读取 res_boxsimu_2 的 DI1。" + }, + { + now: () => "2026-05-22T00:04:56.000Z", + env: { + PATH: process.env.PATH, + HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), + HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", + OPENAI_API_KEY: "must-not-be-used" + }, + callProvider: async () => { + throw new Error("OpenAI fallback must not be used for M3 IO"); + }, + m3IoSkillRequestJson: async (url, request) => { + m3SkillFalseCalls.push({ url, request }); + return { + ok: true, + status: 200, + body: { + status: "succeeded", + accepted: true, + action: "do.write", + traceId: "trc_code-agent-chat-m3-skill-false", + operationId: "op_m3_do_write_false_smoke", + auditId: "aud_m3_do_write_false_smoke_succeeded", + evidenceId: "evd_m3_do_write_false_smoke_succeeded", + auditState: { + status: "written_non_durable" + }, + evidenceState: { + status: "blocked", + sourceKind: "BLOCKED", + blocker: "runtime_durable_not_green", + writeStatus: "written_non_durable" + }, + durableStatus: { + status: "degraded", + durable: false, + blocker: "runtime_durable_not_green" + }, + result: { + value: false, + targetReadback: { + status: "succeeded", + value: false + } + }, + controlPath: { + cloudApi: true, + gatewaySimu: true, + boxSimu: true, + patchPanel: true, + frontendBypass: false + } + } + }; + } + } + ); + validateCodeAgentChatSchema(m3SkillWriteFalse); + assert.equal(m3SkillWriteFalse.status, "completed"); + assert.equal(m3SkillWriteFalse.provider, "hwlab-skill-cli"); + assert.equal(m3SkillWriteFalse.toolCalls[0].route, "/v1/m3/io"); + assert.equal(m3SkillWriteFalse.toolCalls[0].method, "POST"); + assert.equal(m3SkillWriteFalse.toolCalls[0].operationId, "op_m3_do_write_false_smoke"); + assert.equal(m3SkillWriteFalse.toolCalls[0].readback.value, false); + assert.equal(m3SkillWriteFalse.providerTrace.fallbackUsed, false); + assert.match(m3SkillWriteFalse.reply.content, /HWLAB API \/v1\/m3\/io/u); + assert.match(m3SkillWriteFalse.reply.content, /res_boxsimu_1:DO1=false/u); + assert.match(m3SkillWriteFalse.reply.content, /res_boxsimu_2:DI1=false/u); + assert.equal(m3SkillFalseCalls.length, 1); + assert.equal(m3SkillFalseCalls[0].url, "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/m3/io"); + assert.equal(m3SkillFalseCalls[0].request.body.action, "do.write"); + assert.equal(m3SkillFalseCalls[0].request.body.value, false); + logOk("M3 Skill CLI handles user-direct false request through HWLAB API only"); + const directToolCallPayload = JSON.parse(JSON.stringify(m3SkillWrite)); directToolCallPayload.toolCalls[0].hwlabApi = { route: "/v1/m3/io",