Files
012-kaopeilian/docs/规划/流式输出视觉呈现规范.md
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

856 lines
30 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.
# 流式输出视觉呈现规范
> 版本v1.3
> 更新日期2026-01-18
> 适用范围:联系人侧边栏、智能回复等所有 AI 流式输出场景
> 关联规范:[瑞小美AI接入规范](./瑞小美AI接入规范.md)、[技术栈标准](./瑞小美系统技术栈标准与字符标准.md)
---
## 〇、与现有架构的关系
### 后端:基于 AIService.chat_stream()
本规范的 SSE 事件协议是对现有 `AIService.chat_stream()` 的**扩展**,需后端配合改造:
```python
# 现有实现(仅输出 AI 文本)
async for chunk in ai.chat_stream(messages, prompt_name="analysis"):
yield chunk
# 扩展实现(支持本规范的事件协议)
async def enhanced_stream(messages, steps, prompt_name):
# 1. 发送 start 事件
yield sse_event("start", {"request_id": req_id})
# 2. 执行准备步骤并发送 step 事件
for i, step in enumerate(steps):
yield sse_event("step", {"index": i, "status": "active", **step})
await step.execute()
yield sse_event("step", {"index": i, "status": "done", **step})
# 3. 调用 AI 并发送 thinking 事件
async for chunk in ai.chat_stream(messages, prompt_name=prompt_name):
if is_thinking_content(chunk):
yield sse_event("thinking", {"text": chunk})
else:
# 4. 解析结构化结果并发送 field 事件
result = parse_ai_json_response(accumulated_text)
yield sse_event("result_start", {"fields": FIELD_DEFINITIONS})
for key, value in result.items():
yield sse_event("field", {"key": key, "value": value})
# 5. 发送 complete 事件
yield sse_event("complete", {"tokens_used": response.total_tokens})
```
### 前端:扩展现有组件
| 现有组件 | 本规范组件 | 关系 |
|----------|-----------|------|
| `stream.js` | `useAIStream.ts` | 扩展,增加阶段状态管理 |
| `useTypewriter.js` | `TypewriterList.vue` | 复用,用于列表打字效果 |
| `AIStreamDisplay.vue` | `AIStreamContainer.vue` | 升级替换,支持三阶段 |
---
## 一、设计目标
1. **Agent 步骤感**:让用户清晰感知 AI 正在执行的每个步骤
2. **思考过程透明**:展示 AI 的推理过程,增强信任感
3. **渐进式呈现**:结果逐步填充,而非一次性展示
4. **类 Cursor 体验**:思考内容渐隐消失,聚焦最新进展
---
## 二、展示模式
根据 AI 功能的使用场景,分为两种展示模式:
### 2.1 独立页面模式Full Page Mode
适用于功能复杂、结果丰富的 AI 分析,如:消费意向预测、客户画像、项目推荐等。
**特点**
- 占据整个页面/Tab 区域
- 完整展示三阶段(准备→思考→渲染)
- 支持多模块骨架屏渐进填充
### 2.2 内嵌卡片模式Inline Card Mode
适用于轻量级 AI 功能嵌入在列表项、会话卡片等位置AI 会话摘要、快速标签等。
#### 状态机
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ IDLE │ ───▶ │ LOADING │ ───▶ │ COMPLETE │
│ 待触发状态 │ │ 加载中 │ │ 结果展示 │
└─────────────┘ └─────────────┘ └─────────────┘
```
#### 必须满足的行为规范
| 状态 | 必须呈现的元素 | 必须支持的交互 |
|------|---------------|---------------|
| **IDLE** | 可点击的触发入口(如按钮/链接) | 点击后进入 LOADING |
| **LOADING** | 加载指示器 + 提示文案 | 可选:支持取消 |
| **COMPLETE** | AI 生成的结果内容 | 必须:提供"重新生成"入口 |
| **ERROR** | 错误提示 | 必须:可点击重试 |
#### 视觉示意
**IDLE待触发**
```
┌────────────────────────────────────────────────────────┐
│ 19:12 - 19:25 来自: 瑞小美轻医美—农农 10条对话 │
│ │
│ ✨生成AI摘要 | 查看聊天详情
│ │
└────────────────────────────────────────────────────────┘
```
**LOADING加载中**
```
┌────────────────────────────────────────────────────────┐
│ 19:12 - 19:25 来自: 瑞小美轻医美—农农 10条对话 │
│ │
│ ○ 正在生成摘要... │
│ │
└────────────────────────────────────────────────────────┘
```
**COMPLETE结果展示**
```
┌────────────────────────────────────────────────────────┐
│ 19:12 - 19:25 来自: 瑞小美轻医美—农农 10条对话 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 📋 AI会话摘要 [重新生成] │ │
│ │ │ │
│ │ • 员工推送年底消费回馈现金券活动 │ │
│ │ • 客户对活动内容表示疑惑 │ │
│ │ • 客户连续追问未获及时回复 │ │
│ │ • 客户因等待过久表达不满情绪 │ │
│ │ • 邀约到店领券尚未得到响应 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 查看聊天详情
│ │
└────────────────────────────────────────────────────────┘
```
#### 缓存规则
| 场景 | 行为 |
|------|------|
| 无缓存 | 显示 IDLE 状态,等待用户触发 |
| 有缓存 | **直接显示 COMPLETE 状态**,跳过 IDLE |
| 重新生成 | 从 COMPLETE 进入 LOADING完成后更新结果 |
#### 与独立页面模式的区别
| 特性 | 独立页面模式 | 内嵌卡片模式 |
|------|-------------|-------------|
| 空间 | 整个页面/Tab | 嵌入在卡片/列表项内 |
| 准备阶段 | 展示步骤列表 | **省略** |
| 思考阶段 | 展示 `<thinking>` 内容 | **省略** |
| 渲染阶段 | 骨架屏渐进填充 | 直接展示完整结果 |
| 初始触发 | 自动开始 | **等待用户点击** |
---
## 三、三阶段状态机(独立页面模式)
> 以下内容适用于**独立页面模式**
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PREPARING │ ───▶ │ THINKING │ ───▶ │ RENDERING │ ───▶ COMPLETE
│ 准备阶段 │ │ 思考阶段 │ │ 渲染阶段 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
步骤逐个打勾 步骤完成+思考区域 骨架屏+渐进填充
```
### 3.1 阶段定义
| 阶段 | 常量 | 触发条件 | 结束条件 |
|------|------|----------|----------|
| **准备阶段** | `PREPARING` | 请求开始 | 所有步骤完成 |
| **思考阶段** | `THINKING` | 收到第一个 `thinking` 事件 | 收到 `result_start` 事件 |
| **渲染阶段** | `RENDERING` | 收到 `result_start` 事件 | 收到 `complete` 事件 |
| **完成状态** | `COMPLETE` | 收到 `complete` 事件 | - |
| **错误状态** | `ERROR` | 收到 `error` 事件 或 请求异常 | 用户重试 |
---
## 四、视觉规范(独立页面模式)
### 4.1 准备阶段PREPARING
```
┌────────────────────────────────────────┐
│ 🔄 准备分析数据... │
│ │
│ ✅ 获取客户信息 │ ← 已完成
│ ◐ 查询CRM数据 │ ← 进行中
│ ○ 加载对话记录 │ ← 待完成
│ ○ 启动AI分析 │
│ │
└────────────────────────────────────────┘
```
**必须实现的行为**
| 步骤状态 | 必须呈现的元素 |
|---------|---------------|
| 已完成 (done) | 勾号图标 + 完成态样式 |
| 进行中 (active) | 加载动画 + 高亮样式 |
| 待完成 (pending) | 空心/灰色图标 + 弱化样式 |
**步骤数据结构**
```typescript
interface PrepareStep {
key: string
text: string
}
```
---
### 4.2 思考阶段THINKING
```
┌────────────────────────────────────────┐
│ 🔄 准备分析数据... │
│ │
│ ✅ 获取客户信息 │
│ ✅ 查询CRM数据 │
│ ✅ 加载对话记录 │
│ ◐ AI 思考中 │ ← 保持转圈
│ │
│ ───────────────────────────────────── │
│ │
│ ┌──────────────────────────────────┐ │
│ │ ▲ 渐隐遮罩 │ │
│ │ ...析客户消费偏好发现近3个月 │ │
│ │ 主要消费集中在光电类项目... │ │
│ │ 检查到院记录上次到店是12月 │ │
│ │ 15日做的是热玛吉面部... │ │
│ │ 结合咨询铺垫记录,客户对抗衰█ │ │ ← 闪烁光标
│ └──────────────────────────────────┘ │
│ │
│ 💡 AI正在分析CRM数据和对话记录... │
└────────────────────────────────────────┘
```
**必须实现的行为**
| 行为 | 说明 |
|------|------|
| 步骤列表保留 | 前序步骤显示完成状态,最后一步保持"AI思考中" |
| 思考内容展示 | 使用淡色字体,区别于正式结果 |
| 自动滚动 | 新内容出现时自动滚动到底部 |
| 渐隐效果 | 顶部内容渐隐消失,仅保留最后几行 |
| 闪烁光标 | 末尾显示闪烁光标,表示内容仍在输出 |
**类 Cursor 效果的核心**
- 固定高度容器,`overflow: hidden`
- 顶部渐变遮罩实现渐隐
- 只显示最后 N 行(建议 5-8 行)
- 内容自动滚动到底部
---
### 4.3 渲染阶段RENDERING
当收到 `result_start` 事件时,页面**平滑切换**到结果视图。
#### 骨架屏示意
```
┌────────────────────────────────────────┐
│ 🛒 可能消费项目 │ ← 标题立即显示
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │░░░░░░░│ │░░░░░░░│ │░░░░░░░│ │ ← 标签骨架
│ └───────┘ └───────┘ └───────┘ │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ ✅ 触发因素 │
│ ├── ░░░░░░░░░░░░░░░░░░░░░░░░ │ ← 列表骨架
│ ├── ░░░░░░░░░░░░░░░░░░ │
│ └── ░░░░░░░░░░░░░░░░░░░░░ │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ ⚠️ 阻碍因素 │
│ ├── ░░░░░░░░░░░░░░░░░░░░░░ │
│ └── ░░░░░░░░░░░░░░░░░░░░░░░░░ │
└────────────────────────────────────────┘
```
**必须实现的行为**
| 行为 | 说明 |
|------|------|
| 阶段切换 | 从思考阶段平滑过渡到渲染阶段,有淡入淡出动画 |
| 骨架屏先行 | 收到 `result_start` 时立即显示所有模块的骨架屏 |
| 模块标题可见 | 每个模块的标题和图标立即显示,内容区为骨架 |
| 渐进填充 | 收到 `field` 事件时,对应模块骨架被实际内容替换 |
| 内容动画 | 内容出现时有渐进动画(打字机效果 或 淡入效果) |
#### 字段定义结构
后端在 `result_start` 事件中声明字段顺序,前端据此渲染骨架:
```typescript
interface FieldDefinition {
key: string
title: string
type: 'tags' | 'list' | 'text'
icon?: string
}
```
#### 字段类型与填充方式
| 类型 | 说明 | 建议填充效果 |
|------|------|-------------|
| `tags` | 标签列表 | 标签逐个淡入 |
| `list` | 文本列表 | 逐行出现,可选打字机效果 |
| `text` | 普通文本 | 打字机效果或直接显示 |
---
## 五、SSE 事件协议
### 5.1 事件类型定义(推荐方案)
| 事件类型 | 说明 | 触发阶段转换 |
|----------|------|--------------|
| `start` | 流开始 | → PREPARING |
| `step` | 步骤状态更新 | - |
| `thinking` | AI 思考内容(流式) | → THINKING |
| `result_start` | 结果开始,声明字段结构 | → RENDERING |
| `field` | 字段值更新 | - |
| `complete` | 流结束 | → COMPLETE |
| `error` | 错误 | → ERROR |
### 5.2 事件数据格式
#### start
```json
{
"event": "start",
"data": {
"request_id": "req_123456",
"message": "开始分析"
}
}
```
#### step
```json
{
"event": "step",
"data": {
"index": 0,
"key": "customer",
"text": "获取客户信息",
"status": "done" // pending | active | done
}
}
```
#### thinking
```json
{
"event": "thinking",
"data": {
"text": "正在分析客户消费偏好发现近3个月..."
}
}
```
**注意**`thinking` 事件会多次发送,每次携带一小段文本,前端累积拼接。
#### result_start
```json
{
"event": "result_start",
"data": {
"fields": [
{ "key": "likely_items", "title": "可能消费项目", "type": "tags" },
{ "key": "trigger_factors", "title": "触发因素", "type": "list" },
{ "key": "blocking_factors", "title": "阻碍因素", "type": "list" },
{ "key": "suggestions", "title": "跟进建议", "type": "list" }
]
}
}
```
#### field
```json
{
"event": "field",
"data": {
"key": "likely_items",
"value": ["光电类项目(创始人卡续费/使用)", "皮肤管理项目", "抗衰项目"]
}
}
```
```json
{
"event": "field",
"data": {
"key": "trigger_factors",
"value": [
"客户已主动预约明天下午2点半到店",
"客户互动意愿强烈(主动说'你也太容易放弃了吧'",
"高净值客户累计消费13万+客单价1.8万+",
"持有创始人卡项,有消费习惯和忠诚度"
]
}
}
```
#### complete
```json
{
"event": "complete",
"data": {
"request_id": "req_123456",
"tokens_used": 1250,
"duration_ms": 3500
}
}
```
#### error
```json
{
"event": "error",
"data": {
"code": "AI_TIMEOUT",
"message": "AI 分析超时,请重试"
}
}
```
### 5.3 完整事件流示例
```
event: start
data: {"request_id": "req_123", "message": "开始分析"}
event: step
data: {"index": 0, "key": "customer", "text": "获取客户信息", "status": "active"}
event: step
data: {"index": 0, "key": "customer", "text": "获取客户信息", "status": "done"}
event: step
data: {"index": 1, "key": "crm", "text": "查询CRM数据", "status": "active"}
event: step
data: {"index": 1, "key": "crm", "text": "查询CRM数据", "status": "done"}
event: step
data: {"index": 2, "key": "chat", "text": "加载对话记录", "status": "active"}
event: step
data: {"index": 2, "key": "chat", "text": "加载对话记录", "status": "done"}
event: step
data: {"index": 3, "key": "ai", "text": "AI 思考中", "status": "active"}
event: thinking
data: {"text": "正在分析客户消费偏好..."}
event: thinking
data: {"text": "发现近3个月主要消费集中在光电类项目..."}
event: thinking
data: {"text": "检查到院记录上次到店是12月15日..."}
event: thinking
data: {"text": "结合咨询铺垫记录,客户对抗衰项目有明确兴趣..."}
event: result_start
data: {"fields": [{"key": "likely_items", "title": "可能消费项目", "type": "tags"}, ...]}
event: field
data: {"key": "likely_items", "value": ["光电类项目", "皮肤管理项目", "抗衰项目"]}
event: field
data: {"key": "trigger_factors", "value": ["客户已主动预约...", "客户互动意愿强烈...", ...]}
event: field
data: {"key": "blocking_factors", "value": ["创始人卡项可能已过期...", "无明确咨询铺垫项目...", ...]}
event: field
data: {"key": "suggestions", "value": ["建议确认创始人卡续期情况", "可推荐当季抗衰活动"]}
event: complete
data: {"request_id": "req_123", "tokens_used": 1250, "duration_ms": 3500}
```
---
## 六、前端组件架构
### 6.1 组件层次(建议)
```
<AIStreamContainer> <!-- 流式输出容器 -->
├── <PrepareSteps /> <!-- 准备步骤组件 -->
├── <ThinkingArea /> <!-- 思考区域组件 -->
└── <ResultRenderer /> <!-- 结果骨架/内容组件 -->
```
### 6.2 状态管理
推荐封装 `useAIStream` Composable管理以下状态
```typescript
// 核心类型定义
type Phase = 'idle' | 'preparing' | 'thinking' | 'rendering' | 'complete' | 'error'
type StepStatus = 'pending' | 'active' | 'done'
interface PrepareStep {
key: string
text: string
}
interface FieldDefinition {
key: string
title: string
type: 'tags' | 'list' | 'text'
icon?: string
}
// Composable 返回值
interface UseAIStreamReturn {
phase: Ref<Phase> // 当前阶段
isLoading: ComputedRef<boolean> // 是否加载中
prepareSteps: ComputedRef<StepWithStatus[]> // 步骤列表(含状态)
thinkingText: Ref<string> // 思考内容
resultFields: Ref<FieldDefinition[]> // 结果字段定义
resultData: Ref<Record<string, unknown>> // 结果数据
error: Ref<Error | null> // 错误信息
start: (url: string, params?: Record<string, unknown>) => Promise<void>
}
```
### 6.3 SSE 事件与状态映射
| SSE 事件 | 状态变更 |
|----------|----------|
| `start` | `phase``preparing` |
| `step` | 更新对应步骤状态 |
| `thinking` | `phase``thinking`,追加 `thinkingText` |
| `result_start` | `phase``rendering`,设置 `resultFields` |
| `field` | 更新 `resultData[key]` |
| `complete` | `phase``complete` |
| `error` | `phase``error`,设置 `error` |
---
## 七、动画效果要求
> 具体动画参数由开发自行决定,以下为必须实现的效果:
### 独立页面模式
| 效果 | 要求 |
|------|------|
| 步骤完成 | 有明显的"打勾"反馈(如弹出、闪烁) |
| 思考滚动 | 新内容出现时自动滚动到底部,旧内容渐隐上移 |
| 阶段切换 | 有过渡动画,避免突兀跳转 |
| 骨架屏 | 有脉冲/闪烁效果表示加载中 |
| 内容填充 | 渐进式出现(打字机或淡入均可) |
### 内嵌卡片模式
| 效果 | 要求 |
|------|------|
| 加载状态 | 有旋转/跳动等动态指示器 |
| 结果展开 | 有展开动画,避免突然出现 |
| 列表项 | 建议逐条淡入,增强流式感 |
---
## 八、错误处理
### 8.1 错误类型
| 错误码 | 说明 | 用户提示 |
|--------|------|----------|
| `NETWORK_ERROR` | 网络异常 | "网络连接失败,请检查网络后重试" |
| `AI_TIMEOUT` | AI 响应超时 | "AI 分析超时,请稍后重试" |
| `AI_QUOTA_EXCEEDED` | API 配额不足 | "AI 服务暂时不可用,请联系管理员" |
| `DATA_NOT_FOUND` | 数据不存在 | "未找到客户数据,请确认客户信息" |
| `PARSE_ERROR` | 解析失败 | "数据解析失败,请重试" |
### 8.2 错误界面
```
┌────────────────────────────────────────┐
│ │
│ ⚠️ │
│ │
│ AI 分析超时,请稍后重试 │
│ │
│ [ 🔄 重试 ] │
│ │
└────────────────────────────────────────┘
```
---
## 九、缓存与重新分析规范(强制)
> ⚠️ **强制要求**:所有 AI 分析功能必须支持缓存机制和重新分析能力
### 9.1 核心原则
| 原则 | 要求 |
|------|------|
| **优先缓存** | 如有有效缓存,**必须**首先加载缓存数据,跳过流式请求 |
| **重新分析** | 页面**必须**提供"重新分析"按钮,允许用户强制刷新 |
| **缓存标识** | 必须展示数据来源(缓存/实时),让用户知晓数据时效 |
### 9.2 缓存状态定义
在三阶段状态机基础上,新增 `CACHED` 状态:
```
┌─────────────┐
│ CACHED │ ◀──── 有有效缓存时直接进入
│ 缓存加载 │
└──────┬──────┘
│ 用户点击"重新分析"
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PREPARING │ ───▶ │ THINKING │ ───▶ │ RENDERING │ ───▶ COMPLETE
└─────────────┘ └─────────────┘ └─────────────┘
```
### 9.3 界面规范
#### 缓存数据展示
```
┌────────────────────────────────────────┐
│ 🛒 消费意向分析 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 💡 数据来源缓存2小时前分析 │ │ ← 缓存标识
│ └──────────────────────────────────┘ │
│ │
│ 📊 消费概率75%(高意向) │
│ 🛍️ 可能项目:光电类、皮肤管理 │
│ ...(完整结果内容)... │
│ │
│ ───────────────────────────────────── │
│ │
│ [ 🔄 重新分析 ] │ ← 必须有此按钮
│ │
└────────────────────────────────────────┘
```
**必须呈现的元素**
| 元素 | 要求 |
|------|------|
| 缓存标识 | 弱化样式,显示缓存时间(如"2小时前分析" |
| 重新分析按钮 | 明确可点击,点击后进入流式请求流程 |
### 9.4 前端实现要求
缓存相关数据结构:
```typescript
interface CachedResult {
data: Record<string, unknown>
cachedAt: string // ISO 8601 时间戳
validUntil: string // 过期时间
}
```
**必须实现的逻辑**
1. 页面加载时优先检查缓存
2. 有效缓存直接展示,跳过流式请求
3. 提供"重新分析"功能,触发 `force_refresh`
### 9.5 后端 API 要求
所有 AI 分析接口必须支持 `force_refresh` 参数:
```
GET /api/sidebar/purchase-intent?external_userid=xxx&force_refresh=true
```
| 参数 | 类型 | 说明 |
|------|------|------|
| `force_refresh` | boolean | `true` 时跳过缓存,强制重新分析 |
**响应中包含缓存元信息**
```json
{
"code": 0,
"data": {
"probability": 0.75,
"likely_items": ["光电类", "皮肤管理"],
"...": "..."
},
"meta": {
"from_cache": true,
"cached_at": "2026-01-18T10:30:00+08:00",
"valid_until": "2026-01-19T10:30:00+08:00"
}
}
```
### 9.6 缓存有效期建议
| 分析类型 | 建议有效期 | 说明 |
|----------|-----------|------|
| 消费意向预测 | 24 小时 | 数据变化不频繁 |
| 客户画像 | 12 小时 | 对话可能更新 |
| 项目推荐 | 24 小时 | 依赖消费历史 |
| 接待策略 | 2 小时 | 到店场景需实时 |
---
## 十、AI 响应解析与容错
> 参考:[瑞小美AI接入规范](./瑞小美AI接入规范.md) - AI 响应解析规范章节
### 10.1 后端解析要求
AI 返回的 JSON 可能存在格式问题,后端**必须**使用公共解析函数:
```python
from shared_backend.services.ai_service import parse_ai_json_response, safe_parse_ai_json
# 在发送 field 事件前解析 AI 输出
try:
result, thinking = parse_ai_json_response(ai_response.content)
# 发送 thinking 事件(如有)
if thinking:
yield sse_event("thinking", {"text": thinking})
# 发送 result_start 和 field 事件
yield sse_event("result_start", {"fields": FIELD_DEFINITIONS})
for key, value in result.items():
yield sse_event("field", {"key": key, "value": value})
except json.JSONDecodeError as e:
# 解析失败,发送错误事件
yield sse_event("error", {
"code": "PARSE_ERROR",
"message": f"AI 输出解析失败: {str(e)}"
})
```
### 10.2 推荐:使用 json-repair 库
对于 JSON 格式问题较多的场景,推荐使用 `json-repair`GitHub 4000+ 星):
```python
from json_repair import loads as json_repair_loads
# 自动修复常见 JSON 格式问题
result = json_repair_loads(ai_raw_output)
```
**能修复的问题**
- 单引号 → 双引号
- 未闭合括号补齐
- `True/False/None``true/false/null`
- 尾部逗号移除
- Markdown 代码块提取
### 10.3 前端容错处理
前端收到 `field` 事件时,应做类型校验:
```typescript
function onField(data: { key: string; value: unknown }): void {
const { key, value } = data
// 类型校验
const fieldDef = resultFields.value.find(f => f.key === key)
if (!fieldDef) return
// 根据类型校验 value
if (fieldDef.type === 'tags' && !Array.isArray(value)) {
console.warn(`字段 ${key} 期望数组,实际: ${typeof value}`)
resultData.value[key] = []
return
}
if (fieldDef.type === 'list' && !Array.isArray(value)) {
console.warn(`字段 ${key} 期望数组,实际: ${typeof value}`)
resultData.value[key] = []
return
}
resultData.value[key] = value
}
```
---
## 十一、性能优化建议
1. **防抖思考文本更新**:每 50ms 批量更新一次 DOM避免频繁重绘
2. **虚拟滚动**:思考区域内容过长时使用虚拟滚动
3. **骨架屏预渲染**:结果骨架使用 `v-show` 而非 `v-if`
4. **动画硬件加速**:使用 `transform``opacity`,避免触发重排
---
## 十二、兼容性说明
- **SSE 支持**:所有现代浏览器均支持 `fetch` + `ReadableStream`
- **CSS 动画**:使用标准 CSS3 动画,兼容 Chrome 60+, Safari 12+, Firefox 55+
- **字体回退**:等宽字体使用系统回退链
- **字符编码**UTF-8符合[技术栈标准](./瑞小美系统技术栈标准与字符标准.md#字符标准)
```css
font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Code", "Consolas", monospace;
```
---
## 十三、待确认事项
- [ ] 后端 SSE 事件协议改造排期
- [ ] 是否需要支持中断/取消流式请求
- [ ] 移动端适配细节(触摸滚动、字体大小)
- [x] ~~是否需要保存分析历史~~ → 已确认:必须支持缓存,详见第九章
---
*最后更新2026-01-18*