feat(chat): cut over to single SSE endpoint

This commit is contained in:
zhuyongxin
2026-07-22 10:01:12 +08:00
parent f8809cb7dd
commit bc36248cd8
36 changed files with 2187 additions and 945 deletions
@@ -0,0 +1 @@
archive-ready
@@ -0,0 +1 @@
committed
@@ -0,0 +1 @@
schema: spec-driven
@@ -0,0 +1,122 @@
## Context
公开 Chat 仍有同步 `/api/chat` 和伪流式 `/api/chat_stream` 两条旧 `ChatService` 链路;Controller 直接读取 Session 历史、获取模型/Tools、选择策略并维护 cached thread pool。阶段 6A 已完成 protocol-neutral `ChatApplicationUseCase`、observer 和 `ChatRunControl`,但阶段 2-6A 的 plain Java components 尚未形成生产 Bean graph。前端同时保留 quick JSON 与 stream SSE 两套消费者。
本 change 是 L4 破坏性协议迁移。ISS-014 已冻结 event schema、无兼容分支和阶段 7 清理边界;本阶段必须原子切换 Controller、生产装配与前端,不能出现旧路径绕过 release boundary。
## Goals / Non-Goals
**Goals:**
- 唯一 `POST /api/chat` 返回 named SSE events,固定顺序和互斥规则。
- Controller 只负责校验、SSE/HTTP 和连接生命周期,业务执行只调用 `ChatApplicationUseCase`。
- 使用同一个 Run control 处理断开/timeout/send failure,并禁止断开后的终态发送。
- 生产装配阶段 2-6A 的 Core、Tools、Agent、Guards、executors、store 和 use case。
- 使用集中配置与 Spring-managed bounded executors。
- 前端唯一 consumer 严格消费新契约并保存 exact session/run IDs。
**Non-Goals:**
- 不做 Token streaming 或最终内容切片。
- 不修改 `/api/ai_ops` 的 URL、请求或事件协议。
- 不物理删除旧多 Agent、ChatService、Hooks 或 ThreadLocal;阶段 7 处理。
- 不做断线续传、event replay、WebSocket、轮询或 live E2E。
## Decisions
### 1. Named SSE event 是唯一协议层类型
新增 immutable SSE payload:`metadata(session_id,run_id)`、`status(code,message)`、`content(content_type,payload)`、`failure(code,message)`、`done(outcome)`。`SseEmitter.event().name(type).data(payload)` 直接使用业务 event name,不再发送旧 `message + {type,data}` 包装。
替代继续用统一 `message` event;拒绝,因为它允许 type 漂移并让浏览器保留旧兼容分支。`content.payload` 直接引用阶段 6A sealed typed content,禁止 Map/Object 临时协议。
### 2. 每个连接使用显式 first-terminal-wins SSE session
新增 protocol adapter 持有 emitter、`AtomicReference<ChatRunControl>`、disconnect flag、metadata flag 和 terminal flag。`onStarted` 固定发送 metadata;若 disconnect 已先发生则立即取消新得到的 Run control。status 只能在 metadata 后、terminal 前发送。success 原子发送 content + done 后 complete;failure 原子发送 failure + done 后 complete。
`onTimeout`、`onError`、异常 send 和非正常 `onCompletion` 只标记 disconnected 并调用 `cancelClientDisconnect()`;不尝试补发 failure/done。正常 complete 在回调前标记 terminal,避免把成功误判为断开。替代仅在 IOException catch 取消;拒绝,因为 timeout 和 disconnect-before-onStarted 会漏掉。
### 3. 请求接纳失败不进入 SSE 事件序列
空 query 在创建 emitter/Run 前返回 HTTP 400;bounded worker rejection 在创建 Run 前返回 HTTP 503。五事件顺序只适用于已接纳并成功持久化 Run 的请求,因而 metadata 始终是第一个 SSE event。Session ID 格式错误由 use case 形成有 Run 的稳定 failure,除非它在 startRun 前失败;Controller 只做 query shape 校验。
### 4. Chat Controller 不拥有模型、Tools、Session 或业务执行
删除同步 Chat 方法、`/chat_stream`、ChatResponse、历史 helper、内容切片和 Chat 使用的 `ChatService/SessionManager/ChatModel/ToolCallbackProvider`。Controller 构造注入 `ChatApplicationUseCase` 和 qualifier Chat TaskExecutor。
同一类中的 `/api/ai_ops` 仍需旧模型/Tools。为保持其公开协议不变又清理 Controller 依赖,向 `AiOpsService` 增加内部 overload,由 service 注入 ChatModel/ToolCallbackProvider 并调用既有执行方法;Controller 只传 request/session/run。替代本阶段迁移 AiOps 到新 use case;拒绝,因为超出阶段 6B。
### 5. Production assembly 使用一个显式 Bean graph
新增 `HarnessChatConfiguration`,按依赖顺序装配:
```text
properties/Clock/executors
-> DiagnosisHarnessCore + HarnessRetryExecutor + GuardModelCall
-> RedisCanonicalInvocationStore + ToolBoundary
-> RAG/log/MySQL adapters + HarnessEvidenceTools
-> DiagnosisAgentFactory/UseCase
-> EvidenceGuard/Repair + SemanticGuard + ReleaseUseCase
-> Router + System/Knowledge/Diagnosis executors
-> ChatApplicationUseCase(JpaChatRunStore)
```
所有模型入口复用同一 Spring `ChatModel` 和 model executor。RAG/log adapters 复用现有 `LookupKnowledgeTool`/`QueryLogsTools` legacy executors,但所有 Agent-facing output 仍经过 ToolBoundary projection。MySQL 从 `MysqlToolProperties` 生成独立 datasource map;空配置时 Tool 仍注册但 fail closed 为 unknown datasource。
### 6. 并发与预算全部集中配置
新增 `harness.chat` configuration properties,覆盖 Chat worker/model pool、Run deadline/budget、canonical TTL/bytes、Agent/Router/Guard/single-turn/Knowledge limits 和 key prefix。worker 与 model executor 均使用 bounded `ThreadPoolExecutor + AbortPolicy`,由 Spring 负责 shutdown;Controller 不持有或创建 executor。
Retry attempt audit 首版写结构化 debug log,不新增持久化表。替代硬编码构造参数;拒绝,因为阶段 0 已要求上限集中可校准。
### 7. 前端删除 quick/stream 双轨
移除 mode selector DOM、`currentMode`、`sendQuickMessage` 和旧 `/chat_stream` parser。`sendMessage` 只调用 `sendChatMessage`,POST `/api/chat` 并按空行分隔完整 SSE frame,解析 named event 与 JSON payload。metadata 更新 trace target;status 更新等待说明;content 一次渲染 typed payload;failure 显示安全消息;done 校验 outcome 并结束。
typed payload 使用专用 renderer 转换 SYSTEM_CHAT、KNOWLEDGE、DIAGNOSIS_REPORT、FALLBACK,不把对象隐式拼接为字符串。unknown event、重复 metadata/content/failure/done、content/failure 同时出现或流结束前无 done 均 fail closed。
## Module and Ownership Audit
```text
Browser fetch POST /api/chat
-> ChatController (validation + connection admission)
-> ChatSseSession (event order + connection cancellation)
-> bounded chat worker
-> ChatApplicationUseCase (session/run/routing/dispatch/terminal persistence)
-> Harness Core / Agent / Tools / Guards
-> typed content or stable failure
-> ChatSseSession (single content|failure + done)
```
- Controller owns HTTP/SSE only;SSE session owns event state;Application owns business Run;Core owns cancellation/budget;Store owns MySQL;Harness configuration owns infrastructure graph。
- 数据真理源仍为 `diagnosis_run`;SSE metadata 只透出同一 use case 生成的 IDs。
- 最大生命周期风险是 disconnect 与 onStarted 竞态;atomic control + pending disconnect 解决。
- 最大部署风险是 L4 前后端不同步;Controller/前端必须同一 commit 原子部署,回滚也必须成对。
- 无 ADR 冲突;ISS-013 的 token-like “incremental content”被更新的 ISS-014 明确收敛为实时 status + 单次安全 content。
## Interface Impact
- Level: L4 breaking HTTP/frontend contract。
- Removed: synchronous JSON `POST /api/chat`、`POST /api/chat_stream`、旧 `ChatResponse` 和 message-wrapper Chat SSE。
- Added: SSE `POST /api/chat` with five named events and exact snake_case payload fields。
- Consumers: bundled `static/app.js`、外部 Chat API callers、Controller tests/MockMvc;Trace/feedback continue using metadata IDs。
## Risks / Trade-offs
- [客户端仍调用旧 endpoint] -> 同 commit 迁移 bundled frontend;外部消费者必须按 L4 文档迁移,不提供兼容层。
- [发送中断后 worker 继续] -> every send failure/timeout/error cancels exact Run control;late result blocked by Core。
- [executor 饱和] -> bounded queue + HTTP 503 before Run;不 caller-runs、不无界排队。
- [Spring graph 缺 Bean/歧义] -> focused configuration context test + Maven compile。
- [legacy Tool side effects] -> Agent-facing output 仍经新 boundary;legacy classes stage 7 physically removed。
- [typed diagnosis payload frontend rendering复杂] -> deterministic renderer and JS contract tests/static assertions。
## Migration Plan
1. 部署前确保 V012 已迁移,Redis 与模型配置可用。
2. 同一版本注册 Harness Bean graph、切换 `/api/chat` SSE、删除 `/chat_stream` 并更新 bundled frontend。
3. smoke/focused tests 验证 metadata/status/content|failure/done、same IDs 和 cancellation;live E2E 延后阶段 7。
4. 回滚必须同时回滚 Controller 与 frontend 到阶段 6A commit;数据库 V012 nullable additive 可保留。
## Open Questions
- None。初始数值为可配置默认值,阶段 7 根据 live trace 校准。
@@ -0,0 +1,52 @@
## Why
阶段 6A 已完成内部 Chat Application Use Case,但公开入口仍由 `ChatController` 直接持有旧 `ChatService`、模型、工具、Redis 历史和无界线程池。当前 `/api/chat` 是同步 JSON,`/api/chat_stream` 则在完整答案生成后切片,既绕过新 Harness 释放边界,也让前端维持两套不一致消费者。
## What Changes
- 将唯一 `POST /api/chat` 原子切换为 `text/event-stream`,只调用阶段 6A `ChatApplicationUseCase`。
- 删除同步 Chat 响应路径和 `/api/chat_stream`,Controller 不再获取 ChatModel、ToolCallbackProvider、旧 Session 历史或 ChatService。
- 固定五类 SSE 事件与顺序:`metadata -> status* -> content|failure -> done`;业务 event name 与 payload schema 均稳定。
- `content` 最多一次且只承载阶段 6A typed safe content,不做 Token/字符切片;技术失败使用一次 `failure`。
- 客户端断开、SSE timeout 或发送失败取消同一个 `ChatRunControl`,内部 Run 记录 CANCELLED,断开后不再发送终态。
- 使用 Spring 管理的有界 Chat worker 和 Harness model executor;Controller 不创建线程池。
- 新增完整 Harness 生产 Bean 装配,使 Router、三类 executor、Diagnosis Agent、Guards、canonical Tool boundary 和 JPA store 使用同一 Core/ChatModel/ObjectMapper。
- 前端删除快速/流式双模式,统一以 fetch streaming 消费 `/api/chat` 的 named SSE events,并保存 metadata 中的 session/run ID。
## Capabilities
### New Capabilities
- `single-react-chat-sse-cutover`: 定义唯一 Chat SSE 入口、五事件状态机、连接取消、生产 Harness 装配和前端消费者迁移。
### Modified Capabilities
- None. 阶段 6A application use case 和 `/api/ai_ops` 对外行为保持不变。
## Scope
- `ChatController` Chat 入口、SSE DTO/adapter、连接生命周期和 focused MVC tests。
- Harness 生产配置、集中 limits、受控 executors 和装配启动测试。
- `app.js`/`index.html` Chat consumer 和模式控件清理。
- SSE schema/order/mutual exclusion/cancel/failure tests,以及阶段 2-6A regression。
## Non-goals
- 不修改 `/api/ai_ops` 协议或迁移其旧多 Agent 实现。
- 不删除旧 ChatService、Planner/Executor/Verifier/Composer、Hook 或 ThreadLocal;阶段 7 物理清理。
- 不实现 Token streaming、断线续传、事件重放、轮询或 WebSocket。
- 不运行真实模型/Redis/MySQL live E2E;阶段 7 统一完成。
## Context Constraints
- `Diagnosis Agent` 是唯一拥有 Tool loop 的业务 Agent;Controller 和 SSE adapter 不得拥有模型或工具。
- SemanticGuard 完成前不得发送 `content`;Fallback 使用 `content + done(FALLBACK)`,技术失败使用 `failure + done(FAILED)`。
- metadata 固定第一且一次,done 固定最后且一次;content/failure 互斥。
- 这是 L4 破坏性公开协议变更,不保留旧 endpoint 或同步兼容分支。
## Risks
- 阶段 6A 组件尚未生产装配,单改 Controller 会导致 Spring 启动失败。
- SseEmitter completion/error/timeout 与 worker 完成存在竞态,必须 first-terminal-wins 且断开后禁止继续发送。
- 前端解析器当前按单行 data 和旧 `type` 字段兼容,迁移不完整会丢失 metadata、终态或错误。
- 有界 executor 饱和时必须 fail closed,不能退化为 Controller 线程执行或无界排队。
@@ -0,0 +1,123 @@
## ADDED Requirements
### Requirement: Chat SHALL expose one SSE endpoint
The public Chat API SHALL expose only `POST /api/chat` with `text/event-stream`. The synchronous JSON Chat path and `POST /api/chat_stream` MUST be removed without a compatibility branch.
#### Scenario: Accepted Chat request
- **WHEN** a client posts a non-blank Chat query
- **THEN** `/api/chat` returns an SSE emitter backed only by `ChatApplicationUseCase`
#### Scenario: Removed legacy endpoint
- **WHEN** a client posts to `/api/chat_stream`
- **THEN** no Chat handler is mapped to that endpoint
#### Scenario: Invalid request
- **WHEN** a client posts a blank Chat query
- **THEN** the server rejects it before creating a Run or starting an SSE event sequence
### Requirement: SSE events SHALL follow one strict state machine
Every accepted Chat stream SHALL emit named events in the order `metadata -> status* -> content|failure -> done`. Metadata and done MUST occur exactly once, status MAY occur zero or more times, content and failure MUST be mutually exclusive and each MUST occur at most once.
#### Scenario: Successful diagnosis
- **WHEN** a Diagnosis Run safely releases a report
- **THEN** the stream emits one metadata event, zero or more safe status events, one content event, and one SUCCESS done event in order
#### Scenario: Safe fallback
- **WHEN** the release boundary returns a Fallback
- **THEN** the stream emits one content event containing only the fixed Fallback followed by one FALLBACK done event
#### Scenario: Technical failure
- **WHEN** routing or Harness execution cannot produce safe content
- **THEN** the stream emits one stable failure event followed by one FAILED done event and emits no content
### Requirement: SSE payloads SHALL be typed and safe
Metadata SHALL contain only `session_id` and `run_id`; status SHALL contain only a fixed code and safe message; content SHALL contain `content_type` plus the stage 6A typed public payload; failure SHALL contain a stable code and safe message; done SHALL contain only `SUCCESS`, `FALLBACK`, or `FAILED` outcome. The stream MUST NOT expose prompts, thoughts, raw Tool data, complete evidence, internal exceptions, vendor errors, stack traces, unverified Drafts, or internal guard reasons.
#### Scenario: Metadata identity
- **WHEN** the use case starts one Run
- **THEN** the metadata IDs exactly match the IDs persisted and returned by that same Run
#### Scenario: Content release boundary
- **WHEN** status events are emitted before SemanticGuard completes
- **THEN** no diagnosis content is emitted until `ChatApplicationUseCase` returns released typed content
#### Scenario: Failure sanitization
- **WHEN** an internal exception contains provider or persistence details
- **THEN** the failure event contains only the stable application code and public message
### Requirement: Final content SHALL not be disguised token streaming
The Chat stream SHALL send process status while work is running and SHALL release final safe content in one content event. It MUST NOT split a completed answer by character or fixed chunk size.
#### Scenario: Long final report
- **WHEN** a safe final report is larger than the legacy chunk size
- **THEN** it is sent as one typed content event rather than multiple content chunks
### Requirement: Client disconnect SHALL cancel the exact Run
SSE completion before terminal release, timeout, error, or send failure SHALL request CLIENT_DISCONNECT cancellation through the `ChatRunControl` belonging to the same metadata IDs. A disconnect that occurs before `onStarted` MUST be remembered and applied when the control becomes available. No event SHALL be sent after disconnection.
#### Scenario: Disconnect during model work
- **WHEN** the client disconnects after metadata while a model call is pending
- **THEN** the same Run is cancelled, late content is discarded, and no content/failure/done is attempted
#### Scenario: Disconnect before Run control publication
- **WHEN** a disconnect is observed before the application observer receives `onStarted`
- **THEN** the observer cancels that Run immediately when its control is published
#### Scenario: Normal completion callback
- **WHEN** content or failure and done have completed normally
- **THEN** the emitter completion callback does not change the already terminal Run outcome
### Requirement: Controller SHALL remain a bounded protocol adapter
The Controller SHALL validate request shape, admit work to a Spring-managed bounded executor, map application observer/results to SSE, and manage connection lifecycle. It MUST NOT select or invoke ChatModel, ToolCallbackProvider, ChatService routing, Session history, Diagnosis Agent, Guards, or Tool logic, and MUST NOT construct an executor.
#### Scenario: Worker saturation
- **WHEN** the bounded Chat worker rejects a request before a Run starts
- **THEN** the endpoint returns a stable unavailable HTTP response and does not run work on the request thread
#### Scenario: Dependency inspection
- **WHEN** Chat Controller dependencies and source are inspected
- **THEN** only protocol/application collaborators are present for Chat and no model, Tool, old strategy, history, or executor construction remains
### Requirement: Production SHALL assemble one Harness graph
Spring production configuration SHALL assemble one shared Core, ChatModel boundary, canonical store, Tool boundary, three evidence adapters, Diagnosis Agent, EvidenceGuard, repair, SemanticGuard, release use case, three fixed executors, JPA Run store, Router, and Chat Application Use Case. All Run paths SHALL share the same Core and model configuration.
#### Scenario: Application context wiring
- **WHEN** a focused Spring context loads the Harness Chat configuration with controlled dependencies
- **THEN** exactly one `ChatApplicationUseCase` graph is created without missing or ambiguous beans
#### Scenario: MySQL Tool is not configured
- **WHEN** no logical MySQL datasource is configured
- **THEN** the query_mysql Tool remains bounded and fails closed for unknown data sources without using the application database
### Requirement: Runtime limits and executors SHALL be centralized and bounded
Run budgets, timeouts, byte limits, canonical TTL/capacity, worker sizing and model executor sizing SHALL come from `harness.chat` configuration with positive validated defaults. Worker and model executors SHALL use bounded queues and rejection policies and SHALL be shut down by Spring.
#### Scenario: Configuration defaults
- **WHEN** the production properties load without overrides
- **THEN** every Harness limit is positive, canonical limits preserve their invariants, and both executor queues have finite capacity
#### Scenario: Model executor saturation
- **WHEN** the model executor cannot accept another submitted model call
- **THEN** the operation fails as a controlled technical failure and does not create an unbounded queue or caller-runs execution
### Requirement: Bundled frontend SHALL consume only the new Chat SSE contract
The bundled frontend SHALL send every Chat message to `/api/chat`, parse complete named SSE frames, store metadata IDs for Trace/feedback, render safe typed content once, surface stable failure, and finish only after a valid done event. It MUST NOT retain quick/stream mode selection, synchronous Chat JSON parsing, `/chat_stream`, legacy message-wrapper fallback, or raw non-JSON content fallback.
#### Scenario: Frontend success
- **WHEN** the browser receives metadata, statuses, one typed content and SUCCESS done
- **THEN** it records the exact IDs, updates progress, renders the payload once, and marks the message complete
#### Scenario: Frontend protocol violation
- **WHEN** the browser receives an unknown, duplicate, out-of-order, mutually conflicting, or malformed event
- **THEN** it fails closed and does not render the payload as trusted assistant content
#### Scenario: Frontend request target
- **WHEN** static Chat consumer source is inspected
- **THEN** it contains one `/chat` streaming request and no `/chat_stream` or synchronous Chat consumer
### Requirement: AiOps public behavior SHALL remain isolated
The `/api/ai_ops` URL, request schema and event behavior SHALL remain unchanged in stage 6B. Any internal model/Tool dependency movement needed to keep Chat Controller protocol-only MUST preserve existing AiOps observable behavior.
#### Scenario: AiOps regression
- **WHEN** existing AiOps Controller and service tests run after Chat cutover
- **THEN** existing metadata and analysis behavior remains compatible
@@ -0,0 +1,25 @@
## 1. Production Harness assembly
- [x] 1.1 Add validated `harness.chat` properties and Spring-managed bounded Chat/model executors with deterministic shutdown and rejection behavior.
- [x] 1.2 Assemble canonical Redis store, ToolBoundary, bounded RAG/log/MySQL adapters and HarnessEvidenceTools without using the application database for query_mysql.
- [x] 1.3 Assemble shared Core/model boundary, Diagnosis Agent, Guards/release, Router, three fixed executors and one ChatApplicationUseCase graph.
- [x] 1.4 Add focused configuration tests for positive defaults, bounded queues, shared dependencies, empty MySQL fail-closed behavior and unambiguous context startup.
## 2. Unique Chat SSE protocol
- [x] 2.1 Add immutable five-event payloads and a first-terminal-wins SSE session enforcing metadata -> status* -> content|failure -> done order, safe fields and exact Run cancellation.
- [x] 2.2 Replace synchronous `/api/chat` with the bounded-worker SSE path, remove `/api/chat_stream`, old Chat response/chunk/history logic and Chat model/tool/service dependencies.
- [x] 2.3 Move legacy AiOps model/Tool acquisition behind AiOpsService while preserving `/api/ai_ops` observable behavior and using a managed executor.
- [x] 2.4 Add focused Controller/protocol tests for validation, one endpoint, event order/schema/mutual exclusion, typed success/fallback, stable failure, saturation and disconnect races.
## 3. Frontend consumer cutover
- [x] 3.1 Remove quick/stream mode state, selector UI, synchronous Chat JSON path, `/chat_stream` and legacy message-wrapper compatibility.
- [x] 3.2 Implement one strict named-event frame parser and typed content renderer that records metadata IDs, shows status, renders content once and requires valid done.
- [x] 3.3 Add static frontend contract tests covering the unique request target, removed legacy tokens and required event/render branches.
## 4. Verification and migration
- [x] 4.1 Run stage 6B focused tests, relevant stage 2-6A and AiOps regressions, Maven compile and Spring context wiring verification.
- [x] 4.2 Validate strict OpenSpec, no `/chat_stream`/sync consumer/controller model/tool dependency, bounded executors, exact SSE safety fields and no unverified/internal content leakage.
- [x] 4.3 Record L4 frontend/API migration and paired rollback evidence; leave live model/Redis/MySQL E2E explicitly deferred to stage 7.