Merge pull request #2455 from pikasTech/feat/hwlab-trace-stream-debug
Pipelines as Code CI / hwlab-nc01-v03-ci-poll- Success
Pipelines as Code CI / hwlab-nc01-v03-ci-poll- Success
feat: add trace kafka debug streams
This commit is contained in:
@@ -8,6 +8,7 @@ import { Kafka, logLevel } from "kafkajs";
|
|||||||
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
||||||
const DEFAULT_AGENTRUN_EVENT_TOPIC = "agentrun.event.v1";
|
const DEFAULT_AGENTRUN_EVENT_TOPIC = "agentrun.event.v1";
|
||||||
const DEFAULT_HWLAB_EVENT_TOPIC = "hwlab.event.v1";
|
const DEFAULT_HWLAB_EVENT_TOPIC = "hwlab.event.v1";
|
||||||
|
const DEFAULT_STDIO_TOPIC = "codex-stdio.raw.v1";
|
||||||
const DEFAULT_CLIENT_ID = "hwlab-v03-cloud-api";
|
const DEFAULT_CLIENT_ID = "hwlab-v03-cloud-api";
|
||||||
const DEFAULT_GROUP_ID = "hwlab-v03-agentrun-event-bridge";
|
const DEFAULT_GROUP_ID = "hwlab-v03-agentrun-event-bridge";
|
||||||
const DEFAULT_QUERY_TIMEOUT_MS = 5000;
|
const DEFAULT_QUERY_TIMEOUT_MS = 5000;
|
||||||
@@ -305,17 +306,24 @@ function eventFieldCandidates(value, name) {
|
|||||||
const context = objectValue(value.context);
|
const context = objectValue(value.context);
|
||||||
const run = objectValue(value.run);
|
const run = objectValue(value.run);
|
||||||
const command = objectValue(value.command);
|
const command = objectValue(value.command);
|
||||||
|
const payload = objectValue(value.payload);
|
||||||
|
const metadata = objectValue(value.metadata ?? value.meta);
|
||||||
|
const trace = objectValue(value.trace);
|
||||||
|
const ids = objectValue(value.ids);
|
||||||
|
const stdio = objectValue(value.stdio ?? value.frame ?? value.codexStdio);
|
||||||
const sourceEvent = objectValue(value.sourceEvent);
|
const sourceEvent = objectValue(value.sourceEvent);
|
||||||
const nestedEvent = objectValue(value.event?.sourceEvent ?? value.agentRunEvent);
|
const nestedEvent = objectValue(value.event?.sourceEvent ?? value.agentRunEvent ?? payload.event);
|
||||||
if (name === "traceId") return textCandidates(value.traceId, event.traceId, context.traceId, sourceEvent.traceId, nestedEvent.traceId, nestedEvent.payload?.traceId);
|
const nestedPayload = objectValue(nestedEvent.payload);
|
||||||
if (name === "sessionId") return textCandidates(value.sessionId, event.sessionId, context.sessionId, run.sessionId, nestedEvent.payload?.sessionId);
|
if (name === "traceId") return textCandidates(value.traceId, value.trace_id, event.traceId, event.trace_id, context.traceId, context.trace_id, sourceEvent.traceId, sourceEvent.trace_id, nestedEvent.traceId, nestedEvent.trace_id, nestedPayload.traceId, nestedPayload.trace_id, payload.traceId, payload.trace_id, metadata.traceId, metadata.trace_id, trace.traceId, trace.trace_id, ids.traceId, ids.trace_id, stdio.traceId, stdio.trace_id);
|
||||||
if (name === "runId") return textCandidates(value.runId, event.runId, context.runId, run.runId, nestedEvent.runId, nestedEvent.payload?.runId);
|
if (name === "sessionId") return textCandidates(value.sessionId, value.session_id, event.sessionId, event.session_id, context.sessionId, context.session_id, run.sessionId, run.session_id, nestedPayload.sessionId, nestedPayload.session_id, payload.sessionId, payload.session_id, metadata.sessionId, metadata.session_id, ids.sessionId, ids.session_id, stdio.sessionId, stdio.session_id);
|
||||||
if (name === "commandId") return textCandidates(value.commandId, event.commandId, context.commandId, command.commandId, nestedEvent.commandId, nestedEvent.payload?.commandId);
|
if (name === "runId") return textCandidates(value.runId, value.run_id, event.runId, event.run_id, context.runId, context.run_id, run.runId, run.run_id, nestedEvent.runId, nestedEvent.run_id, nestedPayload.runId, nestedPayload.run_id, payload.runId, payload.run_id, metadata.runId, metadata.run_id, ids.runId, ids.run_id, stdio.runId, stdio.run_id);
|
||||||
|
if (name === "commandId") return textCandidates(value.commandId, value.command_id, event.commandId, event.command_id, context.commandId, context.command_id, command.commandId, command.command_id, nestedEvent.commandId, nestedEvent.command_id, nestedPayload.commandId, nestedPayload.command_id, payload.commandId, payload.command_id, metadata.commandId, metadata.command_id, ids.commandId, ids.command_id, stdio.commandId, stdio.command_id);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function kafkaTopicForStream(stream, env) {
|
function kafkaTopicForStream(stream, env) {
|
||||||
const normalized = stringValue(stream) || "hwlab";
|
const normalized = stringValue(stream) || "hwlab";
|
||||||
|
if (normalized === "stdio" || normalized === "codex-stdio") return stringValue(env.HWLAB_KAFKA_STDIO_TOPIC ?? env.AGENTRUN_KAFKA_STDIO_TOPIC) || DEFAULT_STDIO_TOPIC;
|
||||||
if (normalized === "agentrun") return stringValue(env.HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC) || DEFAULT_AGENTRUN_EVENT_TOPIC;
|
if (normalized === "agentrun") return stringValue(env.HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC) || DEFAULT_AGENTRUN_EVENT_TOPIC;
|
||||||
if (normalized === "hwlab") return stringValue(env.HWLAB_KAFKA_EVENT_TOPIC) || DEFAULT_HWLAB_EVENT_TOPIC;
|
if (normalized === "hwlab") return stringValue(env.HWLAB_KAFKA_EVENT_TOPIC) || DEFAULT_HWLAB_EVENT_TOPIC;
|
||||||
return normalized;
|
return normalized;
|
||||||
|
|||||||
@@ -38,11 +38,46 @@ test("workbench Kafka SSE debug endpoint streams filtered raw HWLAB Kafka events
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("workbench Kafka SSE debug endpoint streams filtered codex stdio Kafka events", async () => {
|
||||||
|
const fakeKafka = createFakeKafkaFactory();
|
||||||
|
const env = {
|
||||||
|
HWLAB_KAFKA_BOOTSTRAP_SERVERS: "127.0.0.1:9092",
|
||||||
|
HWLAB_KAFKA_CLIENT_ID: "test-hwlab-cloud-api",
|
||||||
|
HWLAB_KAFKA_STDIO_TOPIC: "codex-stdio.raw.v1"
|
||||||
|
};
|
||||||
|
const server = createServer((request, response) => {
|
||||||
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
||||||
|
void handleWorkbenchKafkaSseDebugHttp(request, response, url, { env, kafkaFactory: fakeKafka.factory, logger: null });
|
||||||
|
});
|
||||||
|
await listen(server);
|
||||||
|
const abort = new AbortController();
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${serverUrl(server)}/v1/workbench/debug/kafka-sse/events?stream=stdio&traceId=trc_stdio_sse_debug&fromBeginning=true`, { signal: abort.signal });
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
assert.ok(reader, "SSE response must expose a readable stream");
|
||||||
|
const connected = await readUntil(reader, "hwlab.kafka.connected");
|
||||||
|
assert.match(connected, /codex-stdio\.raw\.v1/u);
|
||||||
|
|
||||||
|
await fakeKafka.emit({ traceId: "trc_other", stream: "stdout", line: "ignored" });
|
||||||
|
await fakeKafka.emit({ trace_id: "trc_stdio_sse_debug", metadata: { session_id: "ses_stdio_sse_debug", run_id: "run_stdio_sse_debug" }, stream: "stdout", line: "visible stdio frame" });
|
||||||
|
const streamed = await readUntil(reader, "visible stdio frame");
|
||||||
|
assert.match(streamed, /hwlab\.kafka\.event/u);
|
||||||
|
assert.match(streamed, /codex-stdio\.raw\.v1/u);
|
||||||
|
assert.match(streamed, /ses_stdio_sse_debug/u);
|
||||||
|
assert.doesNotMatch(streamed, /trc_other/u);
|
||||||
|
} finally {
|
||||||
|
abort.abort();
|
||||||
|
await close(server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function createFakeKafkaFactory() {
|
function createFakeKafkaFactory() {
|
||||||
let eachMessage: ((input: any) => Promise<void>) | null = null;
|
let eachMessage: ((input: any) => Promise<void>) | null = null;
|
||||||
|
let subscribedTopic = "hwlab.event.v1";
|
||||||
const consumer = {
|
const consumer = {
|
||||||
connect: async () => undefined,
|
connect: async () => undefined,
|
||||||
subscribe: async () => undefined,
|
subscribe: async (input: { topic?: string }) => { subscribedTopic = String(input.topic ?? subscribedTopic); },
|
||||||
run: async (input: any) => { eachMessage = input.eachMessage; },
|
run: async (input: any) => { eachMessage = input.eachMessage; },
|
||||||
stop: async () => undefined,
|
stop: async () => undefined,
|
||||||
disconnect: async () => undefined
|
disconnect: async () => undefined
|
||||||
@@ -52,7 +87,7 @@ function createFakeKafkaFactory() {
|
|||||||
emit: async (value: Record<string, unknown>) => {
|
emit: async (value: Record<string, unknown>) => {
|
||||||
assert.ok(eachMessage, "consumer.run must be called before emitting fake Kafka events");
|
assert.ok(eachMessage, "consumer.run must be called before emitting fake Kafka events");
|
||||||
await eachMessage({
|
await eachMessage({
|
||||||
topic: "hwlab.event.v1",
|
topic: subscribedTopic,
|
||||||
partition: 0,
|
partition: 0,
|
||||||
message: {
|
message: {
|
||||||
offset: String(value.sessionId === "ses_other" ? 1 : 2),
|
offset: String(value.sessionId === "ses_other" ? 1 : 2),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { openKafkaEventStream } from "./kafka-event-bridge.ts";
|
|||||||
|
|
||||||
const CONTRACT_VERSION = "workbench-debug-kafka-sse-v1";
|
const CONTRACT_VERSION = "workbench-debug-kafka-sse-v1";
|
||||||
const DEFAULT_STREAM = "hwlab";
|
const DEFAULT_STREAM = "hwlab";
|
||||||
const ALLOWED_STREAMS = new Set(["hwlab", "agentrun"]);
|
const ALLOWED_STREAMS = new Set(["stdio", "agentrun", "hwlab"]);
|
||||||
|
|
||||||
export async function handleWorkbenchKafkaSseDebugHttp(request, response, url, options = {}) {
|
export async function handleWorkbenchKafkaSseDebugHttp(request, response, url, options = {}) {
|
||||||
const route = routeSuffix(url.pathname);
|
const route = routeSuffix(url.pathname);
|
||||||
@@ -137,6 +137,7 @@ function filtersFromUrl(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function topicForStream(stream, env) {
|
function topicForStream(stream, env) {
|
||||||
|
if (stream === "stdio") return textValue(env.HWLAB_KAFKA_STDIO_TOPIC ?? env.AGENTRUN_KAFKA_STDIO_TOPIC) || "codex-stdio.raw.v1";
|
||||||
if (stream === "agentrun") return textValue(env.HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC) || "agentrun.event.v1";
|
if (stream === "agentrun") return textValue(env.HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC) || "agentrun.event.v1";
|
||||||
return textValue(env.HWLAB_KAFKA_EVENT_TOPIC) || "hwlab.event.v1";
|
return textValue(env.HWLAB_KAFKA_EVENT_TOPIC) || "hwlab.event.v1";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ export interface WorkbenchKafkaSseDebugFilters {
|
|||||||
commandId?: string;
|
commandId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkbenchKafkaSseDebugStreamName = "stdio" | "agentrun" | "hwlab";
|
||||||
|
|
||||||
export interface WorkbenchKafkaSseDebugEvent {
|
export interface WorkbenchKafkaSseDebugEvent {
|
||||||
ok?: boolean;
|
ok?: boolean;
|
||||||
contractVersion?: string;
|
contractVersion?: string;
|
||||||
@@ -74,7 +76,7 @@ export interface WorkbenchKafkaSseDebugEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchKafkaSseDebugStreamOptions extends WorkbenchKafkaSseDebugFilters {
|
export interface WorkbenchKafkaSseDebugStreamOptions extends WorkbenchKafkaSseDebugFilters {
|
||||||
stream?: "hwlab" | "agentrun";
|
stream?: WorkbenchKafkaSseDebugStreamName;
|
||||||
fromBeginning?: boolean;
|
fromBeginning?: boolean;
|
||||||
onEvent: (event: WorkbenchKafkaSseDebugEvent, eventName: string) => void;
|
onEvent: (event: WorkbenchKafkaSseDebugEvent, eventName: string) => void;
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Responsibility: Workbench debug route for backend-driven fake SSE single-step Trace card inspection.
|
// Responsibility: Workbench debug route for backend-driven fake SSE single-step Trace card inspection.
|
||||||
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
import { connectWorkbenchDebugFakeSse, connectWorkbenchKafkaSseDebug, workbenchDebugAPI, type WorkbenchDebugFakeSseQueue, type WorkbenchDebugFakeSseSequence, type WorkbenchDebugFakeSseStream, type WorkbenchKafkaSseDebugEvent } from "@/api";
|
import { connectWorkbenchDebugFakeSse, connectWorkbenchKafkaSseDebug, workbenchDebugAPI, type WorkbenchDebugFakeSseQueue, type WorkbenchDebugFakeSseSequence, type WorkbenchDebugFakeSseStream, type WorkbenchKafkaSseDebugEvent, type WorkbenchKafkaSseDebugStreamName } from "@/api";
|
||||||
import StatusBadge from "@/components/common/StatusBadge.vue";
|
import StatusBadge from "@/components/common/StatusBadge.vue";
|
||||||
import WorkbenchMessageCard from "@/components/workbench/WorkbenchMessageCard.vue";
|
import WorkbenchMessageCard from "@/components/workbench/WorkbenchMessageCard.vue";
|
||||||
import type { WorkbenchRealtimeEvent } from "@/api/workbench-events";
|
import type { WorkbenchRealtimeEvent } from "@/api/workbench-events";
|
||||||
@@ -17,15 +17,26 @@ const queue = ref<WorkbenchDebugFakeSseQueue | null>(null);
|
|||||||
const projection = ref(createWorkbenchDebugFakeSseState());
|
const projection = ref(createWorkbenchDebugFakeSseState());
|
||||||
const streamStatus = ref<"connecting" | "open" | "error" | "closed">("connecting");
|
const streamStatus = ref<"connecting" | "open" | "error" | "closed">("connecting");
|
||||||
const kafkaStatus = ref<"idle" | "connecting" | "open" | "error" | "closed">("idle");
|
const kafkaStatus = ref<"idle" | "connecting" | "open" | "error" | "closed">("idle");
|
||||||
const kafkaStreamName = ref<"hwlab" | "agentrun">("hwlab");
|
const kafkaStreamName = ref<WorkbenchKafkaSseDebugStreamName>("hwlab");
|
||||||
const kafkaFromBeginning = ref(false);
|
const kafkaFromBeginning = ref(false);
|
||||||
const kafkaFilters = ref({ traceId: "", sessionId: "", runId: "", commandId: "" });
|
const kafkaFilters = ref({ traceId: "", sessionId: "", runId: "", commandId: "" });
|
||||||
const kafkaEvents = ref<KafkaRawRow[]>([]);
|
const kafkaEvents = ref<KafkaRawRow[]>([]);
|
||||||
|
const kafkaTraceStatuses = ref<Record<WorkbenchKafkaSseDebugStreamName, KafkaStreamStatus>>(createKafkaStatusMap());
|
||||||
|
const kafkaTraceEvents = ref<Record<WorkbenchKafkaSseDebugStreamName, KafkaRawRow[]>>(createKafkaEventMap());
|
||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const appendDraft = ref(defaultAppendDraft());
|
const appendDraft = ref(defaultAppendDraft());
|
||||||
let stream: WorkbenchDebugFakeSseStream | null = null;
|
let stream: WorkbenchDebugFakeSseStream | null = null;
|
||||||
let kafkaStream: WorkbenchDebugFakeSseStream | null = null;
|
let kafkaStream: WorkbenchDebugFakeSseStream | null = null;
|
||||||
|
const kafkaTraceStreams: Partial<Record<WorkbenchKafkaSseDebugStreamName, WorkbenchDebugFakeSseStream>> = {};
|
||||||
|
|
||||||
|
const KAFKA_TRACE_STREAMS: Array<{ name: WorkbenchKafkaSseDebugStreamName; label: string; topic: string }> = [
|
||||||
|
{ name: "stdio", label: "codex stdio", topic: "codex-stdio.raw.v1" },
|
||||||
|
{ name: "agentrun", label: "AgentRun event", topic: "agentrun.event.v1" },
|
||||||
|
{ name: "hwlab", label: "HWLAB event", topic: "hwlab.event.v1" }
|
||||||
|
];
|
||||||
|
|
||||||
|
type KafkaStreamStatus = "idle" | "connecting" | "open" | "error" | "closed";
|
||||||
|
|
||||||
interface KafkaRawRow {
|
interface KafkaRawRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -39,6 +50,8 @@ const logs = computed(() => projection.value.logs);
|
|||||||
const queueSummary = computed(() => queue.value ? `${queue.value.cursor}/${queue.value.eventCount}` : "-");
|
const queueSummary = computed(() => queue.value ? `${queue.value.cursor}/${queue.value.eventCount}` : "-");
|
||||||
const streamBadgeStatus = computed(() => streamStatus.value === "open" ? "completed" : streamStatus.value === "error" ? "failed" : "running");
|
const streamBadgeStatus = computed(() => streamStatus.value === "open" ? "completed" : streamStatus.value === "error" ? "failed" : "running");
|
||||||
const kafkaBadgeStatus = computed(() => kafkaStatus.value === "open" ? "completed" : kafkaStatus.value === "error" ? "failed" : "running");
|
const kafkaBadgeStatus = computed(() => kafkaStatus.value === "open" ? "completed" : kafkaStatus.value === "error" ? "failed" : "running");
|
||||||
|
const kafkaTraceEventCount = computed(() => Object.values(kafkaTraceEvents.value).reduce((total, rows) => total + rows.length, 0));
|
||||||
|
const kafkaTraceConnectedCount = computed(() => Object.values(kafkaTraceStatuses.value).filter((status) => status === "open").length);
|
||||||
const canStep = computed(() => !busy.value && (queue.value?.remaining ?? 0) > 0);
|
const canStep = computed(() => !busy.value && (queue.value?.remaining ?? 0) > 0);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -52,6 +65,7 @@ onBeforeUnmount(() => {
|
|||||||
stream = null;
|
stream = null;
|
||||||
kafkaStream?.close();
|
kafkaStream?.close();
|
||||||
kafkaStream = null;
|
kafkaStream = null;
|
||||||
|
closeKafkaTraceStreams();
|
||||||
streamStatus.value = "closed";
|
streamStatus.value = "closed";
|
||||||
kafkaStatus.value = "closed";
|
kafkaStatus.value = "closed";
|
||||||
});
|
});
|
||||||
@@ -117,6 +131,62 @@ function clearKafkaEvents(): void {
|
|||||||
kafkaEvents.value = [];
|
kafkaEvents.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openKafkaTraceStreams(): void {
|
||||||
|
closeKafkaTraceStreams();
|
||||||
|
kafkaTraceEvents.value = createKafkaEventMap();
|
||||||
|
kafkaTraceStatuses.value = createKafkaStatusMap("connecting");
|
||||||
|
for (const streamName of KAFKA_TRACE_STREAMS.map((entry) => entry.name)) {
|
||||||
|
const opened = connectWorkbenchKafkaSseDebug({
|
||||||
|
stream: streamName,
|
||||||
|
fromBeginning: true,
|
||||||
|
traceId: cleanFilter(kafkaFilters.value.traceId),
|
||||||
|
sessionId: cleanFilter(kafkaFilters.value.sessionId),
|
||||||
|
runId: cleanFilter(kafkaFilters.value.runId),
|
||||||
|
commandId: cleanFilter(kafkaFilters.value.commandId),
|
||||||
|
onOpen: () => setKafkaTraceStatus(streamName, "open"),
|
||||||
|
onError: () => setKafkaTraceStatus(streamName, "error"),
|
||||||
|
onEvent: (event, eventName) => {
|
||||||
|
if (eventName === "hwlab.kafka.connected") return;
|
||||||
|
if (eventName === "hwlab.kafka.error") {
|
||||||
|
setKafkaTraceStatus(streamName, "error");
|
||||||
|
error.value = event.error?.message || "Kafka SSE 三流订阅错误";
|
||||||
|
}
|
||||||
|
pushKafkaTraceEvent(streamName, { id: kafkaRowId(event, eventName), eventName, receivedAt: new Date().toISOString(), payload: event });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (opened) kafkaTraceStreams[streamName] = opened;
|
||||||
|
else setKafkaTraceStatus(streamName, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeKafkaTraceStreams(): void {
|
||||||
|
for (const streamName of KAFKA_TRACE_STREAMS.map((entry) => entry.name)) {
|
||||||
|
kafkaTraceStreams[streamName]?.close();
|
||||||
|
delete kafkaTraceStreams[streamName];
|
||||||
|
}
|
||||||
|
kafkaTraceStatuses.value = Object.fromEntries(KAFKA_TRACE_STREAMS.map((entry) => [entry.name, kafkaTraceStatuses.value[entry.name] === "idle" ? "idle" : "closed"])) as Record<WorkbenchKafkaSseDebugStreamName, KafkaStreamStatus>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearKafkaTraceEvents(): void {
|
||||||
|
kafkaTraceEvents.value = createKafkaEventMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setKafkaTraceStatus(streamName: WorkbenchKafkaSseDebugStreamName, status: KafkaStreamStatus): void {
|
||||||
|
kafkaTraceStatuses.value = { ...kafkaTraceStatuses.value, [streamName]: status };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushKafkaTraceEvent(streamName: WorkbenchKafkaSseDebugStreamName, row: KafkaRawRow): void {
|
||||||
|
kafkaTraceEvents.value = { ...kafkaTraceEvents.value, [streamName]: [row, ...kafkaTraceEvents.value[streamName]].slice(0, 80) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createKafkaStatusMap(status: KafkaStreamStatus = "idle"): Record<WorkbenchKafkaSseDebugStreamName, KafkaStreamStatus> {
|
||||||
|
return { stdio: status, agentrun: status, hwlab: status };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createKafkaEventMap(): Record<WorkbenchKafkaSseDebugStreamName, KafkaRawRow[]> {
|
||||||
|
return { stdio: [], agentrun: [], hwlab: [] };
|
||||||
|
}
|
||||||
|
|
||||||
async function resetQueue(): Promise<void> {
|
async function resetQueue(): Promise<void> {
|
||||||
busy.value = true;
|
busy.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
@@ -237,7 +307,7 @@ function defaultAppendDraft(): string {
|
|||||||
<div class="workbench-debug-status">
|
<div class="workbench-debug-status">
|
||||||
<StatusBadge v-if="activeTab === 'fake'" :status="streamBadgeStatus" :label="`SSE ${streamStatus}`" />
|
<StatusBadge v-if="activeTab === 'fake'" :status="streamBadgeStatus" :label="`SSE ${streamStatus}`" />
|
||||||
<StatusBadge v-else :status="kafkaBadgeStatus" :label="`Kafka SSE ${kafkaStatus}`" />
|
<StatusBadge v-else :status="kafkaBadgeStatus" :label="`Kafka SSE ${kafkaStatus}`" />
|
||||||
<span class="debug-counter">{{ activeTab === 'fake' ? `Queue ${queueSummary}` : `${kafkaEvents.length} events` }}</span>
|
<span class="debug-counter">{{ activeTab === 'fake' ? `Queue ${queueSummary}` : `${kafkaTraceConnectedCount}/3 streams · ${kafkaTraceEventCount + kafkaEvents.length} events` }}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -262,6 +332,7 @@ function defaultAppendDraft(): string {
|
|||||||
<label class="compact-label">
|
<label class="compact-label">
|
||||||
<span>流</span>
|
<span>流</span>
|
||||||
<select v-model="kafkaStreamName" class="input" :disabled="kafkaStatus === 'connecting' || kafkaStatus === 'open'">
|
<select v-model="kafkaStreamName" class="input" :disabled="kafkaStatus === 'connecting' || kafkaStatus === 'open'">
|
||||||
|
<option value="stdio">codex-stdio.raw.v1</option>
|
||||||
<option value="hwlab">hwlab.event.v1</option>
|
<option value="hwlab">hwlab.event.v1</option>
|
||||||
<option value="agentrun">agentrun.event.v1</option>
|
<option value="agentrun">agentrun.event.v1</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -289,6 +360,9 @@ function defaultAppendDraft(): string {
|
|||||||
<button class="btn btn-primary" type="button" :disabled="kafkaStatus === 'connecting' || kafkaStatus === 'open'" @click="openKafkaStream">连接</button>
|
<button class="btn btn-primary" type="button" :disabled="kafkaStatus === 'connecting' || kafkaStatus === 'open'" @click="openKafkaStream">连接</button>
|
||||||
<button class="btn btn-secondary" type="button" :disabled="kafkaStatus !== 'connecting' && kafkaStatus !== 'open'" @click="closeKafkaStream">断开</button>
|
<button class="btn btn-secondary" type="button" :disabled="kafkaStatus !== 'connecting' && kafkaStatus !== 'open'" @click="closeKafkaStream">断开</button>
|
||||||
<button class="btn btn-secondary" type="button" @click="clearKafkaEvents">清空</button>
|
<button class="btn btn-secondary" type="button" @click="clearKafkaEvents">清空</button>
|
||||||
|
<button class="btn btn-primary" type="button" :disabled="!cleanFilter(kafkaFilters.traceId)" @click="openKafkaTraceStreams">连接三流</button>
|
||||||
|
<button class="btn btn-secondary" type="button" @click="closeKafkaTraceStreams">断开三流</button>
|
||||||
|
<button class="btn btn-secondary" type="button" @click="clearKafkaTraceEvents">清空三流</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p v-if="error" class="workbench-debug-error">{{ error }}</p>
|
<p v-if="error" class="workbench-debug-error">{{ error }}</p>
|
||||||
@@ -324,7 +398,41 @@ function defaultAppendDraft(): string {
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section v-else class="kafka-debug-panel" aria-label="Kafka SSE 原始事件列表">
|
<section v-else class="kafka-debug-layout" aria-label="Kafka SSE 调试面板">
|
||||||
|
<section class="kafka-trace-panel" aria-label="按 traceId 订阅三条 Kafka 流">
|
||||||
|
<div class="debug-section-title">
|
||||||
|
<strong>Trace Streams</strong>
|
||||||
|
<span>{{ kafkaTraceConnectedCount }}/3 open · {{ kafkaTraceEventCount }} events</span>
|
||||||
|
</div>
|
||||||
|
<div class="kafka-stream-grid">
|
||||||
|
<article v-for="streamEntry in KAFKA_TRACE_STREAMS" :key="streamEntry.name" class="kafka-stream-column" :data-stream="streamEntry.name">
|
||||||
|
<header class="kafka-stream-head">
|
||||||
|
<div>
|
||||||
|
<strong>{{ streamEntry.label }}</strong>
|
||||||
|
<code>{{ streamEntry.topic }}</code>
|
||||||
|
</div>
|
||||||
|
<StatusBadge :status="kafkaTraceStatuses[streamEntry.name] === 'open' ? 'completed' : kafkaTraceStatuses[streamEntry.name] === 'error' ? 'failed' : 'running'" :label="kafkaTraceStatuses[streamEntry.name]" />
|
||||||
|
</header>
|
||||||
|
<div v-if="kafkaTraceEvents[streamEntry.name].length === 0" class="debug-empty">等待匹配 traceId 的 {{ streamEntry.topic }} 事件。</div>
|
||||||
|
<ol v-else class="kafka-event-list compact-kafka-event-list">
|
||||||
|
<li v-for="row in kafkaTraceEvents[streamEntry.name]" :key="row.id">
|
||||||
|
<div class="kafka-event-head">
|
||||||
|
<code>{{ row.eventName }}</code>
|
||||||
|
<strong>{{ kafkaEventSummary(row.payload) }}</strong>
|
||||||
|
<span>#{{ row.payload.offset || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="kafka-event-meta">
|
||||||
|
<span>{{ kafkaEventIds(row.payload) }}</span>
|
||||||
|
<time>{{ row.receivedAt }}</time>
|
||||||
|
</div>
|
||||||
|
<pre>{{ kafkaEventJson(row.payload) }}</pre>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="kafka-debug-panel" aria-label="Kafka SSE 原始事件列表">
|
||||||
<div class="debug-section-title">
|
<div class="debug-section-title">
|
||||||
<strong>Raw SSE Events</strong>
|
<strong>Raw SSE Events</strong>
|
||||||
<span>{{ kafkaEvents.length }} events</span>
|
<span>{{ kafkaEvents.length }} events</span>
|
||||||
@@ -344,6 +452,7 @@ function defaultAppendDraft(): string {
|
|||||||
<pre>{{ kafkaEventJson(row.payload) }}</pre>
|
<pre>{{ kafkaEventJson(row.payload) }}</pre>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -567,6 +676,65 @@ function defaultAppendDraft(): string {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kafka-debug-layout {
|
||||||
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-rows: minmax(280px, 0.9fr) minmax(220px, 0.7fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kafka-trace-panel {
|
||||||
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kafka-stream-grid {
|
||||||
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kafka-stream-column {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid #d8e1eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kafka-stream-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kafka-stream-head div {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kafka-stream-head strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kafka-stream-head code {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #0f766e;
|
||||||
|
font-size: 11px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.kafka-debug-panel {
|
.kafka-debug-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -637,11 +805,35 @@ function defaultAppendDraft(): string {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact-kafka-event-list .kafka-event-head {
|
||||||
|
grid-template-columns: minmax(92px, 0.6fr) minmax(110px, 0.8fr) minmax(44px, 0.4fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-kafka-event-list .kafka-event-meta {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-kafka-event-list .kafka-event-meta time {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-kafka-event-list pre {
|
||||||
|
max-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.workbench-debug-grid {
|
.workbench-debug-grid {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kafka-debug-layout {
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kafka-stream-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.debug-side {
|
.debug-side {
|
||||||
grid-template-rows: 280px 220px;
|
grid-template-rows: 280px 220px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user