From 0c10056c22bcc549e3d591dcf9d8a06c8e174f67 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Fri, 5 Jun 2026 00:41:06 +0800 Subject: [PATCH] fix(v02): rebuild terminal user messages (#878) Co-authored-by: Codex Agent --- internal/cloud/access-control.test.ts | 104 ++++++++++++++++++++++++++ internal/cloud/access-control.ts | 32 ++++++-- 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/internal/cloud/access-control.test.ts b/internal/cloud/access-control.test.ts index 26c2ae81..43d63077 100644 --- a/internal/cloud/access-control.test.ts +++ b/internal/cloud/access-control.test.ts @@ -1652,6 +1652,110 @@ test("cloud api terminal workspace sync preserves saved messages and stores runn } }); +test("cloud api terminal workspace sync rebuilds missing user message from saved preview", async () => { + const codeAgentChatResults = new Map(); + const traceId = "trc_issue853_preview_only_trace"; + const conversationId = "cnv_issue853_preview_only"; + const sessionId = "ses_issue853_preview_only"; + const threadId = "thread-issue-853-preview-only"; + const userText = "请执行 hwpod profile list,并在最终回复里原样包含 HWLAB-853-FINAL-preview-only"; + const env = { + HWLAB_ACCESS_CONTROL_REQUIRED: "1", + HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin", + HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass", + HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01", + AGENTRUN_MGR_URL: "http://127.0.0.1:9", + HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1" + }; + const now = () => "2026-06-04T10:35:00.000Z"; + const accessController = createAccessController({ env, now }); + const server = createCloudApiServer({ env, accessController, codeAgentChatResults, now }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" }); + const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue853-preview-only", password: "alice-pass" }, adminLogin.cookie); + assert.equal(aliceCreate.status, 201); + const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue853-preview-only", password: "alice-pass" }); + const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie); + assert.equal(workspace.status, 200); + + await accessController.recordAgentSessionOwner({ + ownerUserId: aliceCreate.body.user.id, + sessionId, + projectId: "prj_device_pod_workbench", + agentId: "hwlab-code-agent", + status: "failed", + conversationId, + threadId, + traceId, + session: { + source: "workbench-status-sync", + sessionStatus: "failed", + lastTraceId: traceId, + messages: [ + { id: "msg_issue853_preview_agent", role: "agent", title: "Code Agent result", text: "HyueAPI 403 INSUFFICIENT_BALANCE", status: "failed", traceId, conversationId, sessionId, threadId, createdAt: "2026-06-04T10:34:59.000Z" } + ], + messageCount: 1, + firstUserMessagePreview: userText, + valuesRedacted: true, + secretMaterialStored: false + } + }); + + codeAgentChatResults.set(traceId, { + status: "failed", + traceId, + conversationId, + sessionId, + threadId, + ownerUserId: aliceCreate.body.user.id, + error: { code: "provider_insufficient_balance", message: "HyueAPI 403 INSUFFICIENT_BALANCE" }, + agentRun: { terminalStatus: "failed" }, + runnerTrace: { + traceId, + status: "failed", + eventCount: 35, + events: [], + lastEvent: { label: "agentrun:result:failed", status: "failed", type: "result" } + }, + session: { sessionId, conversationId, threadId, status: "failed" } + }); + + const selected = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, { + expectedRevision: 1, + selectedConversationId: conversationId, + selectedAgentSessionId: sessionId, + activeTraceId: traceId, + sessionStatus: "running", + messages: [], + updatedByClient: "test-suite" + }, aliceLogin.cookie); + assert.equal(selected.status, 200); + + const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie); + assert.equal(restored.status, 200); + assert.equal(restored.body.workspace.activeTraceId, null); + const messages = restored.body.workspace.selectedConversation.messages; + assert.equal(messages.length, 2); + assert.equal(messages[0].role, "user"); + assert.equal(messages[0].text, userText); + assert.equal(messages[0].traceId, traceId); + assert.equal(messages[1].role, "agent"); + assert.equal(messages[1].status, "failed"); + assert.equal(messages[1].text, "HyueAPI 403 INSUFFICIENT_BALANCE"); + assert.equal(messages[1].runnerTrace.traceId, traceId); + assert.equal(messages[1].runnerTrace.eventCount, 35); + assert.equal(restored.body.workspace.selectedConversation.messageCount, 2); + assert.equal(restored.body.workspace.selectedConversation.firstUserMessagePreview, userText); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +}); + test("access controller restores AgentRun mapping by Code Agent traceId", async () => { const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" }); await accessController.recordAgentSessionOwner({ diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index 64abb50c..e588c1ce 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -2593,13 +2593,14 @@ function isLegacyFinalResponseFallbackText(value) { return textOr(value, "").trim() === "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。"; } function mergeTerminalConversationMessages(existingMessages = [], result = {}, context = {}) { - const messages = Array.isArray(existingMessages) ? existingMessages.map(redactConversationMessage).filter(Boolean) : []; + const baseMessages = Array.isArray(existingMessages) ? existingMessages.map(redactConversationMessage).filter(Boolean) : []; const traceId = safeTraceIdLocal(result.traceId ?? context.traceId); const terminalStatus = terminalWorkbenchSessionStatus(result); const assistantText = textOr(result.reply?.content ?? result.assistantText ?? result.message?.content ?? result.error?.message ?? result.userMessage, ""); const now = textOr(context.now, new Date().toISOString()); + const firstUserPreview = firstUserPreviewFromMessages(baseMessages) ?? textOr(context.firstUserMessagePreview, null); + const messages = ensureTerminalUserMessage(baseMessages, { ...context, traceId, now, firstUserPreview }); const existingCount = numberOrNull(context.existingMessageCount) ?? messages.length; - const firstUserPreview = firstUserPreviewFromMessages(messages) ?? textOr(context.firstUserMessagePreview, null); const runnerTrace = terminalRunnerTraceForConversationMessage(result, { ...context, traceId, terminalStatus }); const next = messages.map((message) => { if (traceId && message.traceId === traceId && message.role === "agent") { @@ -2634,12 +2635,33 @@ function mergeTerminalConversationMessages(existingMessages = [], result = {}, c })); addedAgentMessage = true; } + const finalMessages = next.slice(-50); return { - messages: next.slice(-50), - messageCount: existingCount + (addedAgentMessage ? 1 : 0), - firstUserMessagePreview: firstUserPreviewFromMessages(next) ?? firstUserPreview + messages: finalMessages, + messageCount: Math.max(existingCount + (addedAgentMessage ? 1 : 0), finalMessages.length), + firstUserMessagePreview: firstUserPreviewFromMessages(finalMessages) ?? firstUserPreview }; } +function ensureTerminalUserMessage(messages = [], context = {}) { + if (messages.some((message) => message?.role === "user")) return messages; + const userText = textOr(context.firstUserPreview, ""); + if (!userText) return messages; + const traceId = safeTraceIdLocal(context.traceId); + const userMessage = redactConversationMessage({ + id: traceId ? `msg_${traceId.slice(4)}_user` : `msg_terminal_user_${messages.length}`, + role: "user", + title: "用户", + text: userText, + status: "sent", + traceId, + conversationId: context.conversationId, + sessionId: context.sessionId, + threadId: context.threadId, + createdAt: context.now, + updatedAt: context.now + }); + return userMessage ? [userMessage, ...messages] : messages; +} function terminalRunnerTraceForConversationMessage(result = {}, context = {}) { const source = result.runnerTrace && typeof result.runnerTrace === "object" ? result.runnerTrace : null; if (!source) return null;