feat: 后端统一消息类型分类 + 更新 API 文档
msg_type 现在由后端在所有消息路径上自动填充,前端无需解析内容猜测类型。
- chat_handler: SendSystemMessage → system_info, parseMultiMessage 返回 proactiveSegment{msgType}
- protocol: MultiMessageItem 增加 MsgType 字段
- useWebSocket: 所有 handler 直接读取 msg_type,移除前端类型推断
- docs/api/gateway-api.md: 文档化 msg_type 分类机制,移除已删除的每日简报章节
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -400,12 +400,13 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
|
||||
// 如果已有审查消息则跳过,避免与 review_messages 重复
|
||||
multiParts := parseMultiMessage(fullText)
|
||||
if !hasReview && len(multiParts) > 1 {
|
||||
// 发送 multi_message 事件
|
||||
// 发送 multi_message 事件(每条消息带 msg_type)
|
||||
var items []ws.MultiMessageItem
|
||||
for i, part := range multiParts {
|
||||
for i, seg := range multiParts {
|
||||
items = append(items, ws.MultiMessageItem{
|
||||
Index: i,
|
||||
Content: part,
|
||||
Content: seg.content,
|
||||
MsgType: seg.msgType,
|
||||
})
|
||||
}
|
||||
h.broadcastToUser(client.UserID, ws.ServerMessage{
|
||||
@@ -669,6 +670,8 @@ func (h *ChatHandler) SendSystemMessage(userID, sessionID, text string) error {
|
||||
Type: "response",
|
||||
MessageID: "sys_" + generateID(),
|
||||
Text: text,
|
||||
Role: "system",
|
||||
MsgType: "system_info",
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
@@ -931,18 +934,24 @@ func (h *ChatHandler) broadcastToUser(userID string, msg ws.ServerMessage) {
|
||||
|
||||
// parseMultiMessage 检测并解析多消息格式
|
||||
// 如果文本包含空行分隔的多条消息,拆分为多条;否则返回单条
|
||||
func parseMultiMessage(text string) []string {
|
||||
func parseMultiMessage(text string) []proactiveSegment {
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
// 按双换行(空行)分割
|
||||
parts := strings.Split(text, "\n\n")
|
||||
// 过滤空字符串并去除首尾空白
|
||||
var result []string
|
||||
// 过滤空字符串并去除首尾空白,检测消息类型
|
||||
var result []proactiveSegment
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
msgType := "chat"
|
||||
// 检测括号包裹的动作标记
|
||||
if (strings.HasPrefix(p, "(") && strings.HasSuffix(p, ")")) ||
|
||||
(strings.HasPrefix(p, "(") && strings.HasSuffix(p, ")")) {
|
||||
msgType = "action"
|
||||
}
|
||||
result = append(result, proactiveSegment{msgType: msgType, content: p})
|
||||
}
|
||||
}
|
||||
// 如果只有一条,返回 nil 表示不是多消息格式
|
||||
|
||||
@@ -92,6 +92,7 @@ type MultiMessagePayload struct {
|
||||
type MultiMessageItem struct {
|
||||
Index int `json:"index"`
|
||||
Content string `json:"content"`
|
||||
MsgType string `json:"msg_type,omitempty"` // chat | action | system_info
|
||||
}
|
||||
|
||||
// NotificationInfo 通知推送信息
|
||||
|
||||
+17
-62
@@ -25,10 +25,9 @@ WebSocket 通过 query 参数传 token:`/ws/chat?token=<jwt_token>`
|
||||
10. [自动化规则与场景](#10-自动化规则与场景)
|
||||
11. [通知推送](#11-通知推送)
|
||||
12. [提醒管理](#12-提醒管理)
|
||||
13. [每日简报](#13-每日简报)
|
||||
14. [Webhook 第三方接入](#14-webhook-第三方接入)
|
||||
15. [Admin 管理](#15-admin-管理)
|
||||
16. [健康检查](#16-健康检查)
|
||||
13. [Webhook 第三方接入](#13-webhook-第三方接入)
|
||||
14. [Admin 管理](#14-admin-管理)
|
||||
15. [健康检查](#15-健康检查)
|
||||
|
||||
---
|
||||
|
||||
@@ -210,7 +209,7 @@ ws://<gateway>/ws/chat?token=<jwt>&session_id=<optional>&client_id=<optional>&de
|
||||
"text": "string (完整文本)",
|
||||
"content": "string (增量文本/完整内容)",
|
||||
"role": "user|assistant|action|system",
|
||||
"msg_type": "chat|action|thinking|tool_progress|system_info",
|
||||
"msg_type": "chat|action|thinking|tool_progress|system_info (后端始终填充,前端无需自行解析)",
|
||||
"session_id": "string",
|
||||
"error": "string (仅错误时)",
|
||||
"timestamp": 1717000000000,
|
||||
@@ -221,7 +220,7 @@ ws://<gateway>/ws/chat?token=<jwt>&session_id=<optional>&client_id=<optional>&de
|
||||
"tool_progress": { "tool_name": "string", "status": "started|running|completed|failed", "progress": 0.5, "message": "string" },
|
||||
"system_info": { "level": "info|warning|error", "message": "string", "action": "string" },
|
||||
"notification": { "id": "string", "type": "info|warning|success|thinking|reminder", "title": "string", "body": "string", "timestamp": "string", "data": {} },
|
||||
"multi_message": { "messages": [ { "index": 0, "content": "string" } ] },
|
||||
"multi_message": { "messages": [ { "index": 0, "content": "string", "msg_type": "chat|action|system_info" } ] },
|
||||
"review_messages": [ { "type": "action|chat", "content": "string", "delay_ms": 0 } ],
|
||||
"client_info": { "client_id": "string", "device_name": "string", "user_agent": "string" },
|
||||
"full_audio_url": "string",
|
||||
@@ -251,6 +250,15 @@ ws://<gateway>/ws/chat?token=<jwt>&session_id=<optional>&client_id=<optional>&de
|
||||
| `device_update` | IoT 设备状态更新 |
|
||||
| `background_thinking` | 后台思考状态变更 |
|
||||
|
||||
> **消息类型分类 (`msg_type`)**:所有 `response`、`multi_message`、`history_response`、`stream_chunk`、`thinking`、`tool_progress`、`system_info` 类型的服务端消息中,`msg_type` 字段均由后端自动分类填充,前端只需直接读取 `msg_type` 并据此渲染,无需解析消息内容来猜测类型。
|
||||
>
|
||||
> `msg_type` 可选值:
|
||||
> - `chat` — 普通聊天消息
|
||||
> - `action` — 动作/旁白消息(如 `(昔涟轻轻推开窗户)`),前端以斜体灰色样式渲染
|
||||
> - `thinking` — 后台思考过程,前端显示为可折叠详情块
|
||||
> - `tool_progress` — 工具执行进度,前端显示进度条
|
||||
> - `system_info` — 系统通知,前端居中显示为 toast 样式
|
||||
|
||||
---
|
||||
|
||||
### 流式响应流程
|
||||
@@ -837,60 +845,7 @@ delivered = true 表示目标用户有活跃 WebSocket 连接。
|
||||
|
||||
---
|
||||
|
||||
## 13. 每日简报
|
||||
|
||||
### GET /briefings — 按日期获取
|
||||
|
||||
`?user_id=xxx&date=2024-01-01` (date 默认今天)
|
||||
|
||||
```json
|
||||
// 响应 200
|
||||
{
|
||||
"briefing": {
|
||||
"id": "brief_xxx",
|
||||
"user_id": "string",
|
||||
"date": "2024-01-01",
|
||||
"summary": "AI 生成或回退摘要",
|
||||
"summary_source": "ai|fallback",
|
||||
"status": "pending|generated|delivered",
|
||||
"weather": {
|
||||
"location": "string", "temp": 22.5,
|
||||
"condition": "string", "icon": "☀️"
|
||||
},
|
||||
"news": [
|
||||
{ "title": "string", "url": "string", "source": "string", "summary": "string" }
|
||||
],
|
||||
"reminders": [
|
||||
{ "id": "string", "title": "string", "remind_at": "2024-01-01T15:00:00Z" }
|
||||
],
|
||||
"created_at": "...", "generated_at": "...", "delivered_at": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
briefing 为 null 时:`{"briefing":null, "message":"当日简报尚未生成"}`
|
||||
|
||||
### GET /briefings/latest — 最近简报
|
||||
|
||||
`?user_id=xxx&limit=7 (max 30)`
|
||||
|
||||
```json
|
||||
{ "briefings": [ Briefing ], "total": 7 }
|
||||
```
|
||||
|
||||
### POST /briefings/generate — 手动生成
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{ "user_id": "string (必填)" }
|
||||
|
||||
// 响应 200
|
||||
{ "success": true, "briefing": Briefing, "message": "简报已生成并推送" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Webhook 第三方接入
|
||||
## 13. Webhook 第三方接入
|
||||
|
||||
Auth: `X-Webhook-Key` header。
|
||||
|
||||
@@ -930,7 +885,7 @@ Auth: `X-Webhook-Key` header。
|
||||
|
||||
---
|
||||
|
||||
## 15. Admin 管理
|
||||
## 14. Admin 管理
|
||||
|
||||
需要 JWT + admin 权限(`user_id == "admin"`)。非 admin 返回 **403**。
|
||||
|
||||
@@ -1059,7 +1014,7 @@ Auth: `X-Webhook-Key` header。
|
||||
|
||||
---
|
||||
|
||||
## 16. 健康检查
|
||||
## 15. 健康检查
|
||||
|
||||
### GET /health — 健康检查
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
content: msg.content,
|
||||
timestamp: msg.timestamp,
|
||||
isStreaming: true,
|
||||
msgType: 'chat',
|
||||
});
|
||||
setTyping(false);
|
||||
} else {
|
||||
@@ -248,11 +249,8 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
|
||||
case 'history_response':
|
||||
// 防御性检查:仅当当前消息为空时才加载 WebSocket 历史响应
|
||||
// 避免 WebSocket 的 history_response (可能来自后端空缓存) 覆盖 HTTP loadMessagesFromServer 已加载的消息
|
||||
// 注意:空数组 [] 在 JS 中是 truthy,必须显式检查 length > 0
|
||||
if (msg.messages && msg.messages.length > 0) {
|
||||
const sessionState = useSessionStore.getState();
|
||||
// 如果 sessionStore 或 chatStore 中已有消息,说明 HTTP 已加载完成,忽略 WS 的历史响应
|
||||
if (sessionState.messages.length > 0 || chatState.messages.length > 0) {
|
||||
console.log(
|
||||
'[WS] 忽略 history_response:消息已由 HTTP 加载',
|
||||
@@ -264,8 +262,7 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
const msgsWithIds: Message[] = msg.messages.map((m, i) => ({
|
||||
...m,
|
||||
id: m.id || `hist_${i}_${Date.now()}`,
|
||||
// 规范化 msg_type → msgType 以保持与 HTTP 加载路径一致
|
||||
msgType: (m as any).msg_type || m.msgType,
|
||||
msgType: m.msgType || 'chat',
|
||||
}));
|
||||
setMessages(msgsWithIds);
|
||||
useChatStore.getState().setMessages(msgsWithIds);
|
||||
@@ -294,15 +291,17 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
break;
|
||||
|
||||
case 'multi_message':
|
||||
// 多段消息 — 仅在后端未发送 response 时作为兜底
|
||||
// 多段消息 — 后端已分类 msg_type,前端直接使用
|
||||
if (msg.multi_messages && msg.multi_messages.length > 0) {
|
||||
for (const item of msg.multi_messages) {
|
||||
const itemMsgType = (item.msg_type as MessageDisplayType) || 'chat';
|
||||
addMessage({
|
||||
id: `multi_${Date.now()}_${item.index}`,
|
||||
role: 'assistant',
|
||||
role: itemMsgType === 'action' ? 'action' : 'assistant',
|
||||
content: item.content,
|
||||
timestamp: msg.timestamp || Date.now(),
|
||||
isStreaming: false,
|
||||
msgType: itemMsgType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ export interface IoTDeviceUpdate {
|
||||
export interface MultiMessageItem {
|
||||
index: number;
|
||||
content: string;
|
||||
msg_type?: string; // chat | action | system_info (由后端分类)
|
||||
}
|
||||
|
||||
/** 流式片段 (子会话架构 stream_segments 类型) */
|
||||
|
||||
Reference in New Issue
Block a user