doctorAI/docs/SPEC_chatbox_context_integration.md

345 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPEC: ChatBox 上下文接入与 UI 美化
> **版本**: v1.0
> **前置依赖**: 后端 `POST /deepview/chat` 和 `GET /deepview/chat/history` 已就绪
> **状态**: 草案 (Proposal)
---
## 1. 问题陈述
当前 ChatBox 存在两大核心缺陷:
### 1.1 上下文接入是假的Mock 硬编码)
打开 `app.ts``openChatSheet()``sendChatMessage()` 可知:
- 打招呼消息**硬编码**为"李女士(铂金会员/建档3年"——与当前报告完全无关。
- 用户发消息后,回复也是**硬编码**(永远返回"连续2次拒绝乔雅登"那段话)。
- 没有调用任何后端 API (`POST /deepview/chat`),也没有通过 SSE 接收真实 AI 回复。
- ChatBox 的 `chatId` 没有与当前报告的 `reportId``contextId` 绑定,意味着**所有页面共享同一个假对话**。
### 1.2 ChatBox UI 缺乏角色区分
当前的消息气泡 `.msg-bubble` 仅通过左对齐/右对齐和背景色做了最基础的区分,没有:
- AI 头像(应为方形,体现机器人身份)
- 用户头像(应为圆形,复用 TopBar 的 `avatar-circle` 风格)
- 消息中 Markdown 的富文本渲染AI 回复通常包含列表、加粗等)
### 1.3 ChatBox Header 没有指明上下文来源
Header 固定写死"💬 X光片追踪深打"
- 不知道当前追问的是**哪一份录音报告**,还是**哪位客户的全景档案**。
- 用户无法确认 AI 的回答是否基于正确的上下文。
### 1.4 但后端 SSE 桥接层已经完备Xinzong 审计交叉验证)
经与馨总智能体的 [SSE 事件路由全链路审计](file:///Users/lidongfang/.gemini/antigravity/brain/c489626d-a001-4aff-9e29-13dec0e6ffd4/xinzong_sse_event_routing_audit.md.resolved) 进行交叉比对,确认 **Deepview 后端已完整实现了与 Xinzong 相同的 Hermes SSE 桥接层**(见 `deepview_sse.py` 文件头 L12-19 的翻译规则声明):
| Hermes 原生回调 | Deepview SSE 事件 | 实现位置 | 状态 |
|---|---|---|---|
| `stream_delta_callback(text)` | `agent:chunk {chatId, text}` | `_makeStreamDeltaCallback` L216-233 | ✅ 已实现 |
| `stream_delta_callback(None)` | **丢弃** | L225-226 | ✅ 正确过滤 |
| `tool_progress("tool.started")` | `agent:thinking {chatId, step, message}` | `_makeToolProgressCallback` L246-257 | ✅ 已实现 |
| `tool_progress("tool.completed")` | **丢弃** | L258 | ✅ 正确过滤 |
| `tool_progress("reasoning.available")` | **丢弃** | L258 | ✅ 正确过滤(防重复渲染) |
| `run_conversation()` 返回 | `agent:done {chatId, fullAnswer}` | `_runChat` L627-630 | ✅ 已实现 |
| `run_conversation()` 异常 | `agent:error {chatId, message}` | `_runChat` L634-636 | ✅ 已实现 |
**关键结论**Deepview 的 SSE 管线与 Xinzong 是**同源同构**的。
- 翻译规则完全一致(丢弃 `None`、丢弃 `tool.completed`、丢弃 `reasoning.available`
- `_pushEvent` 基于 `userId` 路由到正确的 SSE 连接
- 前端已注册了 `agent:chunk`、`agent:thinking`、`agent:done` 的 `addEventListener``api.service.ts` L90
**因此,后端需要的改动为零。全部工作量集中在前端的「从 Mock 切换到真实 SSE 消费」**
---
## 2. 改造目标
将 ChatBox 从一个静态 Mock 面板升级为**真实接入后端 Hermes Agent 的上下文对话终端**,同时在 UI 层面达到专业级的对话体验。
---
## 3. 上下文路由机制
### 3.1 chatId 与 contextId 的绑定规则
ChatBox 的核心是两个 ID
| ID | 来源 | 用途 |
|----|------|------|
| `contextId` | 从当前页面 URL 解析 | 告诉后端"AI 应该读哪些文件"作为 RAG 上下文 |
| `chatId` | 前端生成,与 contextId 绑定 | 标识当前对话会话,用于加载历史和持久化 |
**URL → contextId 的解析规则**(在 `app.ts` 路由监听中执行):
```
/deepview/report/rep_xxx → contextId = "recording:{rep_xxx 对应的 asrId}"
或 contextId = "recording:{clientId}/{asrId}" (已归档)
/deepview/client/cli_xxx → contextId = "client:cli_xxx"
```
**chatId 的生成规则**
```
chatId = "chat_" + contextId.replace(/[:/]/g, "_")
```
例如:`chat_recording_abc123` 或 `chat_client_cli_001`
这样保证同一个报告/客户的对话始终复用同一个 chatId回来能看到之前的追问记录。
### 3.2 从报告中提取 contextId
当前 `report-detail.ts` 已经从 API 加载了 `this.report` 对象。该对象中包含 `context_id` 字段(后端 DB `deepview_reports_v2` 表中存储的原始上下文标识)。
**数据流**
```
report-detail.ts 加载 report → 提取 report.context_id →
存入共享 Service → app.ts 在 openChatSheet() 时读取
```
需要一个轻量的 `ChatContextService`(单例 Injectable在 report-detail 和 app.ts 之间桥接。
---
## 4. 前端改造清单
### 4.1 新增 `ChatContextService`
```typescript
// src/app/core/chat-context.service.ts
@Injectable({ providedIn: 'root' })
export class ChatContextService {
/** 当前页面的上下文 ID */
contextId = signal<string>('');
/** 当前上下文的人类可读标题 */
contextTitle = signal<string>('');
/** 上下文类型recording | client */
contextType = signal<'recording' | 'client' | ''>('');
/** 基于 contextId 派生 chatId */
get chatId(): string {
const ctx = this.contextId();
return ctx ? 'chat_' + ctx.replace(/[:/]/g, '_') : '';
}
}
```
### 4.2 修改 `report-detail.ts`
`loadReportFromApi` 成功后:
```typescript
this.chatCtx.contextId.set(data.context_id || `recording:${reportId}`);
this.chatCtx.contextTitle.set(data.clientName || '未归档录音');
this.chatCtx.contextType.set('recording');
```
### 4.3 修改 `app.ts` 的 Chat 逻辑
#### 删除所有 Mock 代码
删除 `openChatSheet()` 中的硬编码欢迎词和 `sendChatMessage()` 中的 `setTimeout` 假回复。
#### `openChatSheet()` 改为:
1.`ChatContextService` 读取 `chatId``contextId`
2. 调用 `GET /deepview/chat/history?chatId=xxx` 加载历史消息。
3. 如果历史为空,推一条系统欢迎消息(基于 `contextType``contextTitle` 动态生成)。
#### `sendChatMessage()` 改为:
1. 将用户消息推入 `chatMessages` 数组(即时展示)。
2. 推入一条 `{ role: 'agent', content: '', isStreaming: true }` 占位。
3. 调用 `POST /deepview/chat` 传递 `{ chatId, text, contextId }`
4. 通过已有的 SSE 监听 (`api.listenToEvents()`) 接收 `agent:chunk``agent:done` 事件,实时追加到占位消息的 content 中。
#### 消息接口扩展:
```typescript
interface ChatMessage {
role: 'user' | 'agent';
content: string;
isStreaming?: boolean; // 正在流式接收中
}
```
### 4.4 `ApiService` 新增方法
```typescript
async getChatHistory(chatId: string): Promise<{messages: any[]}> {
return this.http.get<any>(`${this.apiUrl}/chat/history`, {
headers: this.getHeaders(),
params: { chatId }
}).toPromise();
}
async sendChat(chatId: string, text: string, contextId: string): Promise<{received: boolean}> {
return this.http.post<any>(`${this.apiUrl}/chat`, {
chatId, text, contextId
}, { headers: this.getHeaders() }).toPromise();
}
```
---
## 5. UI 改造规格
### 5.1 ChatBox Header 动态化
**Before**:
```html
<h4>💬 X光片追踪深打</h4>
<p>上下文已在此锁定,免去前提</p>
```
**After**:
```html
<h4>💬 X光片追踪深打</h4>
<p *ngIf="chatContextType === 'recording'">
🎙️ 上下文锁定:本次面诊录音报告
</p>
<p *ngIf="chatContextType === 'client'">
👤 上下文锁定:{{ chatContextTitle }} 全景档案
</p>
```
### 5.2 消息气泡 + 头像重构
**目标效果**
```
┌──────────────────────────────────────┐
│ [ AI ] 消息内容巴拉巴拉... │ ← AI: 方形头像,左对齐
│ ■ │
│ │
│ 用户消息巴拉巴拉... [ 👤 ] │ ← 用户: 圆形头像,右对齐
│ ● │
└──────────────────────────────────────┘
```
**HTML 结构改造** (`app.html` 中 chat-history 区域)
```html
@for (msg of chatMessages; track $index) {
<div class="chat-message" [class.user]="msg.role === 'user'">
<!-- AI 头像:方形 -->
<div class="msg-avatar ai-avatar" *ngIf="msg.role !== 'user'">
<svg>...</svg> <!-- 或用文字 "深" -->
</div>
<div class="msg-bubble" [innerHTML]="msg.content"></div>
<!-- 用户头像:圆形 -->
<div class="msg-avatar user-avatar" *ngIf="msg.role === 'user'">
{{ auth.user()?.name?.charAt(0) || '我' }}
</div>
</div>
}
```
**CSS 规格**
```css
.msg-avatar {
width: 32px; height: 32px;
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 800;
}
/* AI: 方形圆角 + 品牌蓝渐变 */
.ai-avatar {
border-radius: 8px;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: white;
}
/* 用户: 圆形 + 淡紫 */
.user-avatar {
border-radius: 50%;
background: #f3e8ff;
color: #7c3aed;
}
/* AI 消息行:左对齐 */
.chat-message:not(.user) {
justify-content: flex-start;
gap: 8px;
}
/* 用户消息行:右对齐 */
.chat-message.user {
justify-content: flex-end;
gap: 8px;
}
```
### 5.3 流式打字效果Streaming Indicator
当 AI 消息处于 `isStreaming: true` 状态时,在气泡尾部追加呼吸光标:
```css
.msg-bubble.streaming::after {
content: '▋';
animation: blink 1s infinite;
color: var(--color-primary);
}
@keyframes blink {
50% { opacity: 0; }
}
```
---
## 6. 后端现有能力清点(经 Xinzong 审计交叉验证,无需改动)
| 端点/事件 | 实现位置 | 状态 | 说明 |
|------|------|------|------|
| `POST /deepview/chat` | `_handleChat` L482 | ✅ | 接收 `{chatId, text, contextId}`,异步 `create_task(_runChat)` |
| `GET /deepview/chat/history` | `_handleChatHistory` | ✅ | 从 SessionDB 返回 `{messages: [{role, content}, ...]}` |
| SSE `agent:chunk` | `_makeStreamDeltaCallback` L216 | ✅ | 流式推送 `{chatId, text}` |
| SSE `agent:thinking` | `_makeToolProgressCallback` L245 | ✅ | 推送 `{chatId, step, message}`(如"正在读取 profile.md" |
| SSE `agent:done` | `_runChat` L627 | ✅ | 推送 `{chatId, fullAnswer}` 标记回答完成 |
| SSE `agent:error` | `_runChat` L634 | ✅ | 推送 `{chatId, message}` 错误信息 |
### 与 Xinzong 的管线同构性
```
Xinzong SSE Pipeline Deepview SSE Pipeline
═══════════════════ ═════════════════════
stream_delta → chunk ←→ stream_delta → chunk ← 同源
tool.started → thinking ←→ tool.started → thinking ← 同源
tool.completed → 丢弃 ←→ tool.completed → 丢弃 ← 同源
reasoning → 丢弃 ←→ reasoning → 丢弃 ← 同源
None → 丢弃 ←→ None → 丢弃 ← 同源
return → done ←→ return → done ← 同源
exception → error ←→ exception → error ← 同源
```
> [!TIP]
> **试错成本归零**:无需猜测后端推送什么事件、携带什么字段——它们与 Xinzong 完全一致。前端只需将 Xinzong 的 `StateService` 订阅模式(一事件一消费者)复制到 Deepview 的 `app.ts` 中即可。
后端无需任何改动。所有工作在前端 Angular 完成。
---
## 7. 修改文件清单
| 文件 | 动作 | 说明 |
|------|------|------|
| `src/app/core/chat-context.service.ts` | **[NEW]** | 跨组件共享当前上下文 |
| `src/app/core/api.service.ts` | **[MODIFY]** | 新增 `getChatHistory``sendChat` |
| `src/app/pages/report-detail/report-detail.ts` | **[MODIFY]** | 注入 ChatContextService加载报告后设置上下文 |
| `src/app/app.ts` | **[MODIFY]** | 重写 chat 逻辑,接入真实 API 和 SSE |
| `src/app/app.html` | **[MODIFY]** | 重构消息气泡结构(头像 + 动态 Header |
| `src/app/app.css` | **[MODIFY]** | 新增头像、流式光标等样式 |
---
## 8. 验证计划
### 自动验证
1. 打开 `/deepview/report/rep_xxx` 页面,点击 ChatBox观察 Header 是否显示"🎙️ 上下文锁定:本次面诊录音报告"。
2. 发一条消息,确认 `POST /deepview/chat` 被调用且参数中 `contextId` 正确。
3. 确认 SSE `agent:chunk` 事件被正确接收并实时渲染到气泡中。
### 视觉验证
1. AI 消息:方形蓝色头像在左,气泡在右。
2. 用户消息:圆形紫色头像在右,蓝色气泡在左。
3. 流式打字时,气泡尾部有呼吸光标 `▋`
> *本规范指导 ChatBox 从 Mock 阶段进化为真实 AI 对话终端的全部前端改造工作。*