diff --git a/deploy/deploy.schema.json b/deploy/deploy.schema.json index b77b0852..d09c0da3 100644 --- a/deploy/deploy.schema.json +++ b/deploy/deploy.schema.json @@ -587,7 +587,8 @@ "workbenchUiPolicy": { "type": "object", "properties": { - "traceTimeline": { "$ref": "#/$defs/workbenchTraceTimelinePolicy" } + "traceTimeline": { "$ref": "#/$defs/workbenchTraceTimelinePolicy" }, + "rawHwlabEventWindow": { "$ref": "#/$defs/workbenchRawHwlabEventWindowPolicy" } }, "additionalProperties": false }, @@ -599,6 +600,16 @@ }, "additionalProperties": false }, + "workbenchRawHwlabEventWindowPolicy": { + "type": "object", + "required": ["enabled", "maxEntries", "maxRetainedBytes"], + "properties": { + "enabled": { "type": "boolean" }, + "maxEntries": { "type": "integer", "minimum": 1 }, + "maxRetainedBytes": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + }, "externalPostgres": { "type": "object", "required": ["enabled", "serviceName", "endpointAddress", "port"], diff --git a/deploy/deploy.yaml b/deploy/deploy.yaml index 59f8c88b..77725898 100644 --- a/deploy/deploy.yaml +++ b/deploy/deploy.yaml @@ -374,6 +374,10 @@ lanes: traceTimeline: autoExpandRunning: false autoCollapseTerminal: false + rawHwlabEventWindow: + enabled: true + maxEntries: 200 + maxRetainedBytes: 1048576 observability: traceExplorerUrlTemplate: /v1/workbench/traces/{trace_id}/events prometheusOperatorResources: false diff --git a/docs/MDTODO/details/pure-kafka-live-single-step-debug/R2.1_Task_Report.md b/docs/MDTODO/details/pure-kafka-live-single-step-debug/R2.1_Task_Report.md new file mode 100644 index 00000000..3a27a277 --- /dev/null +++ b/docs/MDTODO/details/pure-kafka-live-single-step-debug/R2.1_Task_Report.md @@ -0,0 +1,47 @@ +# R2.1 纯 Kafka Trace 序列权威修复报告 + +## 上下文 + +- 前端根因: + - [HWLAB #2475](https://github.com/pikasTech/HWLAB/issues/2475#issuecomment-4935381417) +- 运行面证据: + - [HWLAB #2477](https://github.com/pikasTech/HWLAB/issues/2477#issuecomment-4935385934) + +## 完成结果 + +- 共享 Trace renderer 按 `runnerTrace.eventSource` 选择序列权威: + - `hwlab-kafka-sse` 使用 `sourceSeq`; + - `trace-api` 和其他 durable 路径继续只使用 `projectedSeq`。 +- live Kafka 事件保持前端 ingress 数组顺序: + - 不按 `sourceSeq` 重新排序; + - 优先以 `sourceEventId` 去重; + - 缺少该字段时使用 `runId + sourceSeq`; + - 不伪造 `projectedSeq`。 +- Trace 行显式携带 `sequenceAuthority`: + - DOM 通用属性为 `data-event-seq` 与 `data-event-seq-authority`; + - 只有 durable 行继续暴露 `data-projected-seq`。 +- live Trace 合并保持: + - source identity 去重; + - terminal 事件不清空既有事件; + - `eventCount` 等于已保存事件数。 + +## 验证 + +- production mapper 验证: + - inner event 携带 `sourceSeq`; + - inner event 不产生 `projectedSeq`。 +- renderer 与状态验证: + - source-only assistant、tool 和 terminal 能生成可读行; + - 行顺序与 ingress 顺序一致; + - durable Trace 缺少 `projectedSeq` 时仍拒绝渲染; + - 重复 `sourceEventId` 不产生重复行。 +- 定向测试结果: + - Bun 后端、renderer、merge 和 SSE API:`50 pass`; + - TraceTimeline 与调试面板组件:`4 pass`; + - v03 GitOps render 目标用例:`1 pass`。 + +## 边界 + +- 未启用或调用 projector、read model、snapshot、sync、recovery replay、gap-fill 或 polling。 +- 未部署运行面。 +- 未修改父 issue [HWLAB #2474](https://github.com/pikasTech/HWLAB/issues/2474)。 diff --git a/docs/MDTODO/details/pure-kafka-live-single-step-debug/R2.2_Task_Report.md b/docs/MDTODO/details/pure-kafka-live-single-step-debug/R2.2_Task_Report.md new file mode 100644 index 00000000..e98f0067 --- /dev/null +++ b/docs/MDTODO/details/pure-kafka-live-single-step-debug/R2.2_Task_Report.md @@ -0,0 +1,76 @@ +# R2.2 原始 HWLAB Event 观察窗报告 + +## 上下文 + +- 主问题: + - [HWLAB #2474](https://github.com/pikasTech/HWLAB/issues/2474) +- 前端设计与最小 seam: + - [HWLAB #2475](https://github.com/pikasTech/HWLAB/issues/2475#issuecomment-4935381417) + +## 完成结果 + +- 现有产品 EventSource 增加单一 ingress tee: + - 每个命名 `hwlab.event.v1` frame 在 decode 和 coalesce 前记录一次; + - 同一次 decode 结果继续进入原生产 reducer; + - 未增加产品 EventSource、Kafka consumer 或第二事实源。 +- 原始观察状态为 bounded 内存: + - 按到达顺序保留完整 `MessageEvent.data`; + - 不按 trace、event type 或 reducer 结果做客户端过滤; + - invalid JSON 和非对象 JSON 保留原文并显示拒绝原因; + - 淘汰完整 envelope,不截断仍被保留的 envelope; + - 不写 localStorage、数据库或 canonical Workbench store。 +- 隔离调试卡片增加独立子标签: + - `Trace 卡片` 保留隔离 debug replay 和生产 reducer 卡片; + - `原始 HWLAB Event` 使用只读纯文本框显示同一产品 ingress; + - 切换标签不创建或关闭网络连接; + - 清空观察窗只清原始内存 buffer。 +- R2.3 v2 UI 集成点已预留: + - 支持 `replayId` 和 output barrier query 参数; + - 接收 `replay-complete.result`; + - 展示 server `scanned/matched/delivered`; + - 用明确标注的 client local overlay 展示 `received/decoded/applied`。 + +## YAML-first + +- owning YAML: + - `deploy/deploy.yaml#lanes.v03.workbench.rawHwlabEventWindow`。 +- 独立字段: + - `enabled`; + - `maxEntries`; + - `maxRetainedBytes`。 +- 渲染链: + - deploy schema 校验字段形状; + - GitOps renderer 生成 Cloud Web env; + - Cloud Web runtime 注入浏览器配置; + - 前端只读取显式值,不设置容量默认值。 +- 特例分类: + - `keep-domain-special`:观察窗保持管理员可见、内存只读; + - `remove-code-default`:容量与开关没有代码默认值; + - `legacy-retire`:本任务没有新增或保留旧入口。 + +## 验证 + +- ingress 与 buffer: + - 每个产品 frame 恰好产生一条 raw ingress 和一次 reducer delivery; + - invalid JSON 可见但不进入 reducer; + - entry/byte eviction 均保持完整 envelope。 +- UI: + - 两个子标签、局部清空和 raw 文本顺序通过; + - 标签切换期间 EventSource 实例数保持不变。 +- 配置与 runtime: + - Cloud Web runtime:`18 pass`; + - v03 GitOps render 目标用例:`1 pass`; + - deploy 合同:`15` 个 JSON 文件验证通过。 +- 定向功能测试: + - Bun 后端、renderer、merge 和 SSE API:`50 pass`; + - TraceTimeline 与调试面板组件:`4 pass`。 +- 已知基线失败: + - renderer 全套仍有两个与本变更无关的旧断言失败; + - Web check 的 source-shape、显式 any 扫描和 Vite build 通过; + - `workbench-isolated-kafka-debug.test.ts:61` 仍有 R2.3 分支已有的类型断言错误。 + +## 边界 + +- 未部署 NC01/v03。 +- 未修改父 issue [HWLAB #2474](https://github.com/pikasTech/HWLAB/issues/2474)。 +- 产品链继续保持纯 Kafka 实时,不增加补洞或投影路径。 diff --git a/docs/MDTODO/pure-kafka-live-single-step-debug.md b/docs/MDTODO/pure-kafka-live-single-step-debug.md index d014ba07..13727bbc 100644 --- a/docs/MDTODO/pure-kafka-live-single-step-debug.md +++ b/docs/MDTODO/pure-kafka-live-single-step-debug.md @@ -19,10 +19,10 @@ ## R2 [in_progress] 修复纯 Kafka 实时 Trace 可读性、隔离调试分层诊断与同一 SSE ingress 原始 HWLAB Event 观察窗;保持产品链无 projector、read model、snapshot、sync、recovery replay 或 polling。上下文:[HWLAB #2474](https://github.com/pikasTech/HWLAB/issues/2474) 与 [调查结论](https://github.com/pikasTech/HWLAB/issues/2474#issuecomment-4935408473),完成任务后将详细报告写入[任务报告](./details/pure-kafka-live-single-step-debug/R2_Task_Report.md)。 -### R2.1 [in_progress] +### R2.1 [completed] 按 eventSource 修复 Trace 序列权威:live Kafka 保留 ingress 顺序并以 sourceEventId 或 runId+sourceSeq 识别事件,不伪造 projectedSeq;补 sourceSeq-only running/assistant/tool/terminal、去重与 terminal 保留合同。上下文:[前端根因 #2475](https://github.com/pikasTech/HWLAB/issues/2475#issuecomment-4935381417)、[运行面证据 #2477](https://github.com/pikasTech/HWLAB/issues/2477#issuecomment-4935385934)。依赖:无。验证:真实 mapper envelope 的 eventCount 等于 stored events、readableRows 大于 0,projection 保持关闭,完成任务后将详细报告写入[任务报告](./details/pure-kafka-live-single-step-debug/R2.1_Task_Report.md)。 -### R2.2 +### R2.2 [completed] 在同一产品 EventSource 的 decode/coalesce 前 tee 原始 frame,并在隔离调试卡片增加 Trace 卡片/原始 HWLAB Event 子标签;raw 按到达顺序写入 bounded 内存、暴露 invalid JSON,YAML-first 开关和容量独立配置,不新增 SSE 或 Kafka consumer。上下文:[主 issue #2474](https://github.com/pikasTech/HWLAB/issues/2474)、[前端设计 #2475](https://github.com/pikasTech/HWLAB/issues/2475#issuecomment-4935381417)。依赖:无,可与 Trace 序列权威修复并行。验证:每个产品 frame 恰好 raw+1 且 reducer 一次,切换标签不新增 EventSource,eviction/invalid-json/runtime contract 通过,完成任务后将详细报告写入[任务报告](./details/pure-kafka-live-single-step-debug/R2.2_Task_Report.md)。 ### R2.3 diff --git a/internal/cloud/kafka-event-bridge.test.ts b/internal/cloud/kafka-event-bridge.test.ts index 8b4aa3e3..9634eee8 100644 --- a/internal/cloud/kafka-event-bridge.test.ts +++ b/internal/cloud/kafka-event-bridge.test.ts @@ -88,6 +88,8 @@ test("projects AgentRun assistant_message Kafka event into HWLAB trace event", ( assert.equal(projected.context.sourceSeq, 7); assert.equal(projected.context.agentRunEventType, "assistant_message"); assert.equal(projected.event.type, "assistant"); + assert.equal(projected.event.sourceSeq, 7); + assert.equal(Object.prototype.hasOwnProperty.call(projected.event, "projectedSeq"), false); assert.equal(projected.event.label, "agentrun:assistant:message"); assert.equal(projected.event.status, "running"); assert.equal(projected.event.text, "partial answer"); diff --git a/internal/dev-entrypoint/cloud-web-runtime.mjs b/internal/dev-entrypoint/cloud-web-runtime.mjs index e63230a0..224d6ca4 100644 --- a/internal/dev-entrypoint/cloud-web-runtime.mjs +++ b/internal/dev-entrypoint/cloud-web-runtime.mjs @@ -396,7 +396,12 @@ function workbenchRuntimeConfigFromEnv() { projectionRealtime: requiredWorkbenchRealtimeFeature(process.env.HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED, "HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED") }, debugCapabilities: { - isolatedKafka: requiredWorkbenchRealtimeFeature(process.env.HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED, "HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED") + isolatedKafka: requiredWorkbenchRealtimeFeature(process.env.HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED, "HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED"), + rawHwlabEventWindow: { + enabled: requiredWorkbenchRealtimeFeature(process.env.HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_ENABLED, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_ENABLED"), + maxEntries: requiredPositiveWorkbenchInteger(process.env.HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_ENTRIES, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_ENTRIES"), + maxRetainedBytes: requiredPositiveWorkbenchInteger(process.env.HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_RETAINED_BYTES, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_RETAINED_BYTES") + } } }; const autoExpandRunning = parseEnvBoolean(process.env.HWLAB_WORKBENCH_TRACE_AUTO_EXPAND_RUNNING); @@ -416,6 +421,12 @@ function requiredWorkbenchRealtimeFeature(value, name) { throw new Error(`${name} is required and must be explicitly true or false`); } +function requiredPositiveWorkbenchInteger(value, name) { + const number = Number(value); + if (!Number.isInteger(number) || number <= 0) throw new Error(`${name} is required and must be a positive integer`); + return number; +} + function traceExplorerUrlTemplateFromEnv(value) { if (value === undefined || value === null || value === "") return null; const template = String(value).trim(); diff --git a/internal/dev-entrypoint/cloud-web-runtime.test.mjs b/internal/dev-entrypoint/cloud-web-runtime.test.mjs index 5a78aa2b..afd75ac9 100644 --- a/internal/dev-entrypoint/cloud-web-runtime.test.mjs +++ b/internal/dev-entrypoint/cloud-web-runtime.test.mjs @@ -432,7 +432,10 @@ test("cloud web serves client deep links through the Vue shell", async () => { HWLAB_CLOUD_WEB_DISPLAY_TIME_LABEL: "北京时间", HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "true", HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "false", - HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED: "true" + HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED: "true", + HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_ENABLED: "true", + HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_ENTRIES: "200", + HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_RETAINED_BYTES: "1048576" }); const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-cloud-web-runtime-")); await writeFile(path.join(root, "index.html"), "
\n", "utf8"); @@ -463,7 +466,7 @@ test("cloud web serves client deep links through the Vue shell", async () => { const html = await response.text(); assert.match(html, /显示最近 {{ renderedRows.length }} / {{ readableRows.length }} 行
{{ row.header }}
diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchKafkaDebugPanel.test.ts b/web/hwlab-cloud-web/src/components/workbench/WorkbenchKafkaDebugPanel.test.ts
index cf36cb9e..2e36bdff 100644
--- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchKafkaDebugPanel.test.ts
+++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchKafkaDebugPanel.test.ts
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
-import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mount } from "@vue/test-utils";
import { nextTick } from "vue";
@@ -19,23 +19,46 @@ describe("WorkbenchKafkaDebugPanel", () => {
});
it("replays only after an explicit click, completes on terminal, and clears locally", async () => {
- const wrapper = mount(WorkbenchKafkaDebugPanel, { props: { traceId: "trc_component_debug" } });
+ const wrapper = mount(WorkbenchKafkaDebugPanel, {
+ props: {
+ traceId: "trc_component_debug",
+ replayId: "rpl_component_debug",
+ outputBarrier: { partition: 0, firstOffset: "10", lastOffset: "11" }
+ }
+ });
expect(FakeEventSource.instances).toHaveLength(0);
await wrapper.get('[data-testid="workbench-kafka-debug-replay"]').trigger("click");
- const source = FakeEventSource.instances[0];
+ const source = FakeEventSource.instances[0]!;
expect(source.url).toContain("stream=hwlab-debug");
expect(source.url).toContain("fromBeginning=true");
expect(source.url).toContain("traceId=trc_component_debug");
+ expect(source.url).toContain("replayId=rpl_component_debug");
+ expect(source.url).toContain("partition=0");
+ expect(source.url).toContain("firstOffset=10");
+ expect(source.url).toContain("lastOffset=11");
source.emit("hwlab.kafka.connected", { consumerReady: true, debugIsolation: true, deliverySemantics: "debug-replay", liveOnly: false, replay: true, topic: "hwlab.event.debug.v1", groupId: "debug-group-unique" });
source.emit("hwlab.kafka.event", debugRecord("assistant", { type: "assistant", assistantText: "debug answer", status: "running" }));
source.emit("hwlab.kafka.event", debugRecord("terminal", { type: "result", eventType: "terminal", terminal: true, status: "completed" }));
- source.emit("hwlab.kafka.replay-complete", { reason: "terminal", terminalObserved: true, count: 2 });
+ source.emit("hwlab.kafka.replay-complete", {
+ reason: "terminal",
+ terminalObserved: true,
+ count: 2,
+ result: {
+ contractVersion: "workbench-kafka-debug-replay-v2",
+ replayId: "rpl_component_debug",
+ code: "terminal_complete",
+ terminalObserved: true,
+ counts: { scanned: 2, matched: 2, delivered: 2, clientReceived: null, decoded: null, applied: null }
+ }
+ });
await nextTick();
expect(wrapper.get('[data-testid="workbench-kafka-debug-panel"]').attributes("data-replay-status")).toBe("completed");
expect(wrapper.get('[data-testid="workbench-kafka-debug-counts"]').text()).toContain("2/2");
+ expect(wrapper.get('[data-testid="workbench-kafka-debug-layer-counts"]').text()).toContain("scanned=2");
+ expect(wrapper.get('[data-testid="workbench-kafka-debug-layer-counts"]').text()).toContain("received=2 · decoded=2 · applied=2");
expect(wrapper.get(".message-card").attributes("data-status")).toBe("completed");
expect(wrapper.get(".message-card").text()).toContain("debug answer");
expect(source.closed).toBe(true);
@@ -45,7 +68,7 @@ describe("WorkbenchKafkaDebugPanel", () => {
expect(wrapper.get('[data-testid="workbench-kafka-debug-counts"]').text()).toContain("0/0");
await wrapper.get('[data-testid="workbench-kafka-debug-replay"]').trigger("click");
- const incompleteSource = FakeEventSource.instances[1];
+ const incompleteSource = FakeEventSource.instances[1]!;
incompleteSource.emit("hwlab.kafka.connected", { consumerReady: true, debugIsolation: true, deliverySemantics: "debug-replay", liveOnly: false, replay: true });
incompleteSource.emit("hwlab.kafka.replay-complete", { reason: "timeout", terminalObserved: false, count: 0 });
await nextTick();
@@ -53,6 +76,67 @@ describe("WorkbenchKafkaDebugPanel", () => {
expect(wrapper.text()).toContain("重放未观测到 terminal(timeout)");
expect(incompleteSource.closed).toBe(true);
});
+
+ it("shows the product raw HWLAB Event buffer without opening another EventSource", async () => {
+ const firstRaw = '{"schema":"hwlab.event.v1","eventId":"evt_raw_1"}';
+ const invalidRaw = "{invalid-json";
+ const clearRawIngress = vi.fn();
+ const wrapper = mount(WorkbenchKafkaDebugPanel, {
+ props: {
+ traceId: "trc_component_raw",
+ replayEnabled: true,
+ rawEnabled: true,
+ onClearRawIngress: clearRawIngress,
+ rawIngress: {
+ scopeKey: "ses_component_raw",
+ receivedCount: 2,
+ decodedCount: 1,
+ rejectedCount: 1,
+ evictedCount: 0,
+ retainedBytes: firstRaw.length + invalidRaw.length,
+ rows: [
+ { arrivalSeq: 1, receivedAt: "2026-07-10T00:00:00.000Z", eventName: "hwlab.event.v1", rawText: firstRaw, decodeStatus: "accepted", byteLength: firstRaw.length },
+ { arrivalSeq: 2, receivedAt: "2026-07-10T00:00:01.000Z", eventName: "hwlab.event.v1", rawText: invalidRaw, decodeStatus: "invalid-json", byteLength: invalidRaw.length }
+ ]
+ }
+ }
+ });
+
+ expect(FakeEventSource.instances).toHaveLength(0);
+ await wrapper.get('[data-testid="workbench-kafka-debug-tab-raw"]').trigger("click");
+ expect(FakeEventSource.instances).toHaveLength(0);
+ expect(wrapper.get('[data-testid="workbench-raw-hwlab-counts"]').text()).toContain("received=2");
+ expect((wrapper.get('[data-testid="workbench-raw-hwlab-text"]').element as HTMLTextAreaElement).value).toBe(`${firstRaw}\n${invalidRaw}`);
+
+ await wrapper.get('[data-testid="workbench-raw-hwlab-clear"]').trigger("click");
+ expect(clearRawIngress).toHaveBeenCalledTimes(1);
+ expect(FakeEventSource.instances).toHaveLength(0);
+ });
+
+ it("downgrades server terminal_complete when the browser received fewer events", async () => {
+ const wrapper = mount(WorkbenchKafkaDebugPanel, { props: { traceId: "trc_component_transport_gap" } });
+ await wrapper.get('[data-testid="workbench-kafka-debug-replay"]').trigger("click");
+ const source = FakeEventSource.instances[0]!;
+ source.emit("hwlab.kafka.connected", { consumerReady: true, debugIsolation: true, deliverySemantics: "debug-replay", liveOnly: false, replay: true });
+ source.emit("hwlab.kafka.event", debugRecord("assistant", { type: "assistant", assistantText: "partial", status: "running" }, "trc_component_transport_gap"));
+ source.emit("hwlab.kafka.replay-complete", {
+ reason: "terminal",
+ terminalObserved: true,
+ result: {
+ contractVersion: "workbench-kafka-debug-replay-v2",
+ code: "terminal_complete",
+ ok: true,
+ terminalObserved: true,
+ counts: { delivered: 2, clientReceived: null, decoded: null, applied: null }
+ }
+ });
+ await nextTick();
+
+ expect(wrapper.get('[data-testid="workbench-kafka-debug-panel"]').attributes("data-replay-status")).toBe("incomplete");
+ expect(wrapper.text()).toContain("transport_failed");
+ expect(wrapper.get('[data-testid="workbench-kafka-debug-layer-counts"]').text()).toContain("received=1 · decoded=1 · applied=1");
+ expect(source.closed).toBe(true);
+ });
});
class FakeEventSource {
@@ -88,7 +172,7 @@ class FakeEventSource {
}
}
-function debugRecord(eventType: string, event: Record{{ traceId || "等待当前 turn" }}
topic {{ connection?.topic || "等待 consumer-ready" }}
group {{ connection?.groupId || "-" }}
+ replayId {{ replayIdLabel }}
+ barrier {{ outputBarrierLabel }}
delivery debug-replay · fromBeginning=true
现有产品 EventSource
+ scope {{ rawIngress.scopeKey || "等待产品 SSE scope" }}
+ delivery hwlab.event.v1 · memory-only · no client event filter
+ {{ visibleError }}
+{{ visibleError }}
-