Files
pikasTech-HWLAB/web/hwlab-cloud-web/src/stores/workbench-isolated-kafka-debug.ts
T

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> : {};
}