fix(workbench): 透传用户消息发送时间

This commit is contained in:
root
2026-07-20 05:37:30 +02:00
parent 9cc1fa99dd
commit a5927a8498
6 changed files with 32 additions and 4 deletions
@@ -1071,6 +1071,11 @@ function rejectRemovedResourceWorkspaceFiles(value) {
throw adapterError("legacy_workspace_files_removed", "workspaceFiles/resourceWorkspaceFiles are removed; use AgentRun ResourceBundleRef kind=gitbundle with bundles[]");
}
function validIsoTimestamp(value) {
const timestamp = firstNonEmpty(value);
return timestamp && Number.isFinite(Date.parse(timestamp)) ? new Date(Date.parse(timestamp)).toISOString() : null;
}
function adapterError(code, message, details = {}) {
return Object.assign(new Error(message), {
code,
@@ -1092,6 +1097,7 @@ function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId,
...(promptEvidence?.prompt ? { promptMetadata: promptEvidence.prompt } : {}),
traceId,
userMessageId: agentRunUserMessageId(params, traceId),
submittedAt: validIsoTimestamp(params.submittedAt),
projectId: firstNonEmpty(params.projectId) || null,
conversationId: safeConversationId(params.conversationId) || null,
sessionId: sessionId ?? scopedAgentRunSessionIdForParams(params, traceId),
@@ -1121,6 +1127,7 @@ function buildAgentRunSteerCommandInput({ params, traceId, steerTraceId, mapped
...(promptEvidence?.prompt ? { promptMetadata: promptEvidence.prompt } : {}),
traceId: steerTraceId,
userMessageId: agentRunUserMessageId(params, steerTraceId),
submittedAt: validIsoTimestamp(params.submittedAt),
targetTraceId: traceId,
targetCommandId: mapped.agentRun.commandId,
conversationId: safeConversationId(mapped.conversationId ?? params.conversationId) || null,
@@ -168,6 +168,10 @@ test("projects only explicit AgentRun lifecycle timestamps", () => {
}, { source: "hwlab-test" });
assert.equal(withoutFacts.event.startedAt, null);
assert.equal(withoutFacts.event.finishedAt, null);
assert.equal(withoutFacts.event.createdAt, "2026-07-09T10:00:05.000Z");
assert.equal(withoutFacts.event.submittedAt, null);
assert.equal(withoutFacts.event.timingClock, "event-created-at-fallback");
assert.equal(withoutFacts.event.blocking, false);
});
test("projects AgentRun assistant_message Kafka event into HWLAB trace event", () => {
@@ -336,6 +340,7 @@ test("projects AgentRun user_message into a stable HWLAB user event", () => {
traceId: "trc_user_message",
targetTraceId: "trc_user_message_target",
userMessageId: "msg_user_message_user",
submittedAt: "2026-07-10T09:59:55.000Z",
text: "replay this input",
source: "agentrun-turn-command-payload",
valuesPrinted: false
@@ -352,6 +357,10 @@ test("projects AgentRun user_message into a stable HWLAB user event", () => {
assert.equal(projected.event.userMessageId, "msg_user_message_user");
assert.equal(projected.event.messageId, "msg_user_message_user");
assert.equal(projected.event.targetTraceId, "trc_user_message_target");
assert.equal(projected.event.createdAt, "2026-07-10T09:59:55.000Z");
assert.equal(projected.event.submittedAt, "2026-07-10T09:59:55.000Z");
assert.equal(projected.event.timingClock, "user-submitted-at");
assert.equal(projected.event.blocking, false);
assert.equal(projected.event.text, "replay this input");
assert.equal(projected.event.sourceEventId, "evt_user_message");
assert.equal(projected.sourceEvent.partition, 1);
+5
View File
@@ -1516,8 +1516,10 @@ function mapAgentRunSourceEventToHwlabEvent({ input, sourceEvent, payload, run,
if (type === "user_message") {
const userMessageId = firstText(payload.userMessageId);
const userText = textPayload(payload, "user message");
const submittedAt = timestampValue(payload.submittedAt);
return {
...base,
createdAt: submittedAt || base.createdAt,
type: "user",
eventType: "user",
status: "sent",
@@ -1529,6 +1531,9 @@ function mapAgentRunSourceEventToHwlabEvent({ input, sourceEvent, payload, run,
userMessageId,
targetTraceId: firstText(payload.targetTraceId),
userSource: firstText(payload.source),
submittedAt,
timingClock: submittedAt ? "user-submitted-at" : "event-created-at-fallback",
blocking: false,
terminal: false
};
}
@@ -21,6 +21,7 @@ test("L1 AgentRun application dispatches gpt.pika without polling or projecting
stateFile,
env: agentRunEnvironment(),
fetchImpl: fakeAgentRunManager(calls),
now: () => "2026-07-20T01:13:12.232Z",
});
const created = await app.createSession({ actor: { id: "usr_native", role: "user" }, params: { providerProfile: "gpt.pika" } });
@@ -29,7 +30,7 @@ test("L1 AgentRun application dispatches gpt.pika without polling or projecting
actor: { id: "usr_native", role: "user" },
traceId: "trc_l1_gpt_pika_smoke",
sessionId: session.sessionId,
params: { message: "hi", providerProfile: "gpt.pika" },
params: { message: "hi", submittedAt: "2026-07-20T01:13:07.699Z", providerProfile: "gpt.pika" },
};
const admitted = await app.admitTurn(input);
const result = await app.dispatchTurn(admitted);
@@ -48,6 +49,7 @@ test("L1 AgentRun application dispatches gpt.pika without polling or projecting
const state = JSON.parse(await readFile(stateFile, "utf8"));
expect(state.turns[input.traceId]).toMatchObject({
status: "running",
createdAt: "2026-07-20T01:13:07.699Z",
terminal: false,
runId: "run_l1_gpt_pika",
commandId: "cmd_l1_gpt_pika",
@@ -94,6 +96,9 @@ function agentRunEnvironment(): Record<string, string> {
AGENTRUN_API_KEY: "test-agentrun-key",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "NC01",
HWLAB_CODE_AGENT_AGENTRUN_GPT_PIKA_SECRET_NAME: "provider-gpt-pika",
HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME: "agentrun-test-tool-github-pr",
HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME: "agentrun-test-tool-unidesk-ssh",
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
HWLAB_CODE_AGENT_AGENTRUN_REPO_URL: "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git",
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "gpt.pika",
@@ -126,6 +131,7 @@ function fakeAgentRunManager(calls: Array<{ method: string; path: string; author
}
if (method === "POST" && url.pathname === "/api/v1/runs/run_l1_gpt_pika/commands") {
expect(body.payload.prompt).toBe("hi");
expect(body.payload.submittedAt).toBe("2026-07-20T01:13:07.699Z");
return send({
id: "cmd_l1_gpt_pika",
runId: "run_l1_gpt_pika",
@@ -76,7 +76,7 @@ export function createNativeAgentRunWorkbenchApplication(options: {
async admitTurn(input) {
return mutate((state) => {
const session = ownedSession(state, input.sessionId, input.actor.id);
const timestamp = now();
const timestamp = validTimestamp(input.params.submittedAt) ?? now();
const message = requiredText(input.params.message, "message");
state.turns[input.traceId] = {
traceId: input.traceId,
@@ -93,7 +93,7 @@ export function createNativeAgentRunWorkbenchApplication(options: {
session.status = "running";
session.lastTraceId = input.traceId;
session.updatedAt = timestamp;
return { ...input, params: { ...input.params, message, providerProfile: text(input.params.providerProfile) || session.providerProfile } };
return { ...input, params: { ...input.params, message, submittedAt: timestamp, providerProfile: text(input.params.providerProfile) || session.providerProfile } };
});
},
async dispatchTurn(input: WorkbenchTurnInput) {
@@ -163,6 +163,7 @@ async function load(stateFile: string): Promise<NativeState> {
}
function object(value: unknown) { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, Record<string, any>> : {}; }
function text(value: unknown) { return String(value ?? "").trim(); }
function validTimestamp(value: unknown) { const timestamp = text(value); return timestamp && Number.isFinite(Date.parse(timestamp)) ? new Date(Date.parse(timestamp)).toISOString() : null; }
function requiredText(value: unknown, field: string) { const result = text(value); if (!result) throw codedError("invalid_input", `${field} is required`); return result; }
function validOptionalId(value: unknown, prefix: string) { const id = text(value); if (!id) return null; if (!id.startsWith(prefix)) throw codedError("invalid_input", `${prefix.slice(0, -1)} id is invalid`); return id; }
function ownedSession(state: NativeState, sessionId: string, ownerUserId: string) { const session = state.sessions[sessionId]; if (!session || session.ownerUserId !== ownerUserId) throw codedError("session_not_found", "session is not visible to the actor"); return session; }
+1 -1
View File
@@ -545,7 +545,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
chatPending.value = true;
rememberRecentDraft(value);
recordActivity("submit");
const payload = { message: value, prompt: value, sessionId, threadId: providerThreadId, traceId, userMessageId, providerProfile: providerProfile.value, gatewayShellTimeoutMs: gatewayShellTimeoutMs.value, targetTraceId: composer.value.targetTraceId, steerTraceId };
const payload = { message: value, prompt: value, submittedAt, sessionId, threadId: providerThreadId, traceId, userMessageId, providerProfile: providerProfile.value, gatewayShellTimeoutMs: gatewayShellTimeoutMs.value, targetTraceId: composer.value.targetTraceId, steerTraceId };
const response = steerMode
? await workbenchColadaMutations.steerAgentMessage({ payload, timeoutMs: codeAgentTimeoutMs.value, activityRef: () => activityRef.value, sessionId, traceId })
: await workbenchColadaMutations.submitAgentMessage({ payload, timeoutMs: codeAgentTimeoutMs.value, activityRef: () => activityRef.value, sessionId, traceId });