107 lines
4.6 KiB
TypeScript
107 lines
4.6 KiB
TypeScript
// Responsibility: pure, isolated projection of hwlab.event.debug.v1 records through the production Workbench reducers.
|
|
// This module never reads or writes the canonical Workbench store.
|
|
|
|
import type { WorkbenchKafkaSseDebugEvent } from "@/api/workbench-debug";
|
|
import type { WorkbenchRealtimeEvent } from "@/api/workbench-events";
|
|
import type { ChatMessage } from "@/types";
|
|
import { firstNonEmptyString } from "@/utils";
|
|
import { projectWorkbenchLiveKafkaMessage } from "./workbench-live-kafka-event";
|
|
|
|
export interface WorkbenchIsolatedKafkaDebugLog {
|
|
id: string;
|
|
eventType: string;
|
|
actionType: string;
|
|
stepTypes: string[];
|
|
traceId: string | null;
|
|
accepted: boolean;
|
|
reason: string | null;
|
|
observedAt: string;
|
|
}
|
|
|
|
export interface WorkbenchIsolatedKafkaDebugState {
|
|
message: ChatMessage | null;
|
|
receivedCount: number;
|
|
appliedCount: number;
|
|
lastOffset: string | null;
|
|
logs: WorkbenchIsolatedKafkaDebugLog[];
|
|
error: string | null;
|
|
}
|
|
|
|
export function createWorkbenchIsolatedKafkaDebugState(): WorkbenchIsolatedKafkaDebugState {
|
|
return { message: null, receivedCount: 0, appliedCount: 0, lastOffset: null, logs: [], error: null };
|
|
}
|
|
|
|
export function applyWorkbenchIsolatedKafkaDebugEvent(
|
|
state: WorkbenchIsolatedKafkaDebugState,
|
|
envelope: WorkbenchKafkaSseDebugEvent,
|
|
expectedTraceId: string | null | undefined
|
|
): WorkbenchIsolatedKafkaDebugState {
|
|
const value = recordValue(envelope.value);
|
|
const debugEvent = value as WorkbenchRealtimeEvent;
|
|
const traceId = realtimeTraceId(debugEvent);
|
|
const expected = firstNonEmptyString(expectedTraceId);
|
|
const receivedCount = state.receivedCount + 1;
|
|
const base = { ...state, receivedCount, lastOffset: firstNonEmptyString(envelope.offset, state.lastOffset) ?? null };
|
|
if (debugEvent.schema !== "hwlab.event.debug.v1") return rejected(base, debugEvent, traceId, "debug-envelope-schema-mismatch");
|
|
if (!traceId) return rejected(base, debugEvent, null, "debug-envelope-trace-missing");
|
|
if (expected && traceId !== expected) return rejected(base, debugEvent, traceId, "debug-envelope-trace-mismatch");
|
|
|
|
const traceEvent = debugEvent.event;
|
|
if (!traceEvent) return withLog(base, debugEvent, traceId, "ignore", [], false, "debug-envelope-not-a-trace-event");
|
|
const message = projectWorkbenchLiveKafkaMessage({
|
|
previous: state.message,
|
|
traceId,
|
|
sessionId: firstNonEmptyString(debugEvent.hwlabSessionId, debugEvent.sessionId, traceEvent.sessionId, state.message?.sessionId) ?? "ses_workbench_isolated_debug",
|
|
event: traceEvent,
|
|
receivedAt: new Date().toISOString(),
|
|
title: "Code Agent · 隔离调试"
|
|
});
|
|
return withLog({ ...base, message, appliedCount: state.appliedCount + 1, error: null }, debugEvent, traceId, "debug.trace.event", ["debug-project-message"], true, null);
|
|
}
|
|
|
|
export function workbenchCurrentDebugTraceId(messages: ChatMessage[], sessionLastTraceId?: string | null): string | null {
|
|
const agentMessages = messages.filter((message) => message.role === "agent");
|
|
const running = [...agentMessages].reverse().find((message) => ["pending", "running"].includes(String(message.status ?? "").trim().toLowerCase()) && firstNonEmptyString(message.traceId, message.runnerTrace?.traceId));
|
|
return firstNonEmptyString(
|
|
running?.traceId,
|
|
running?.runnerTrace?.traceId,
|
|
...[...agentMessages].reverse().flatMap((message) => [message.traceId, message.runnerTrace?.traceId]),
|
|
sessionLastTraceId
|
|
) ?? null;
|
|
}
|
|
|
|
function rejected(state: WorkbenchIsolatedKafkaDebugState, event: WorkbenchRealtimeEvent, traceId: string | null, reason: string): WorkbenchIsolatedKafkaDebugState {
|
|
return withLog(state, event, traceId, "ignore", [], false, reason);
|
|
}
|
|
|
|
function withLog(
|
|
state: WorkbenchIsolatedKafkaDebugState,
|
|
event: WorkbenchRealtimeEvent,
|
|
traceId: string | null,
|
|
actionType: string,
|
|
stepTypes: string[],
|
|
accepted: boolean,
|
|
reason: string | null
|
|
): WorkbenchIsolatedKafkaDebugState {
|
|
const observedAt = new Date().toISOString();
|
|
const log: WorkbenchIsolatedKafkaDebugLog = {
|
|
id: `${observedAt}:${state.receivedCount}:${traceId ?? "none"}`,
|
|
eventType: firstNonEmptyString(event.eventType, event.event?.type, event.type, "unknown") ?? "unknown",
|
|
actionType,
|
|
stepTypes,
|
|
traceId,
|
|
accepted,
|
|
reason,
|
|
observedAt
|
|
};
|
|
return { ...state, error: accepted ? null : reason, logs: [log, ...state.logs].slice(0, 40) };
|
|
}
|
|
|
|
function realtimeTraceId(event: WorkbenchRealtimeEvent): string | null {
|
|
return firstNonEmptyString(event.traceId, event.event?.traceId) ?? null;
|
|
}
|
|
|
|
function recordValue(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|