a058b0ab8e
- 修复记忆管理数据库连接不可用 (ai-core重编译+Unicode修复) - 修复IoT子会话工具调用链路日志缺失 - 新增最终审查子会话(review_provider) 支持消息格式解析拆分 - 实现历史消息持久化(后端存储+前端分页加载) - 前端新增动作消息(ActionMessage)类型和渲染 - 优化对话链路速度(非阻塞子会话+快速问候通道) - JWT密钥环境变量化(无默认值启动panic) - Token自动刷新机制(401拦截器+refresh接口) - WebSocket指数退避重连(jitter+最大10次) - localStorage清理一致性(cyrene_前缀+版本检查) - IoT环境变量统一为IOT_SERVICE_URL
705 lines
19 KiB
Go
705 lines
19 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
|
|
"github.com/yourname/cyrene-ai/gateway/internal/store"
|
|
"github.com/yourname/cyrene-ai/gateway/internal/ws"
|
|
)
|
|
|
|
// SessionHandler 会话管理处理器
|
|
type SessionHandler struct {
|
|
store *store.SessionStore // PostgreSQL 持久化存储
|
|
hub *ws.Hub
|
|
useDB bool // 数据库是否可用
|
|
}
|
|
|
|
// NewSessionHandler 创建会话处理器
|
|
func NewSessionHandler(hub *ws.Hub, s *store.SessionStore) *SessionHandler {
|
|
return &SessionHandler{
|
|
store: s,
|
|
hub: hub,
|
|
useDB: s != nil && s.IsAvailable(),
|
|
}
|
|
}
|
|
|
|
// canAccess 检查当前用户是否有权访问指定 userID 所属的资源
|
|
// admin 用户豁免所有权检查
|
|
func (h *SessionHandler) canAccess(c *gin.Context, resourceOwnerID string) bool {
|
|
currentUserID := middleware.GetUserID(c)
|
|
if middleware.GetIsAdmin(c) {
|
|
return true
|
|
}
|
|
return currentUserID == resourceOwnerID
|
|
}
|
|
|
|
// ========== POST /api/v1/sessions — 创建会话 ==========
|
|
|
|
type createSessionRequest struct {
|
|
UserID string `json:"user_id"`
|
|
SessionID string `json:"session_id"`
|
|
Title string `json:"title"`
|
|
IsMain bool `json:"is_main"`
|
|
}
|
|
|
|
// Create 创建新会话
|
|
func (h *SessionHandler) Create(c *gin.Context) {
|
|
userID := middleware.GetUserID(c)
|
|
|
|
var req createSessionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
// 允许空 body
|
|
}
|
|
if req.Title == "" {
|
|
req.Title = "新的对话"
|
|
}
|
|
if req.SessionID == "" {
|
|
req.SessionID = "session_" + randomID(12)
|
|
}
|
|
|
|
if h.useDB {
|
|
if err := h.store.CreateSession(userID, req.SessionID, req.Title, req.IsMain); err != nil {
|
|
log.Printf("[SessionHandler] 创建会话失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建会话失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"id": req.SessionID,
|
|
"user_id": userID,
|
|
"title": req.Title,
|
|
"is_main": req.IsMain,
|
|
"created_at": time.Now().UnixMilli(),
|
|
"updated_at": time.Now().UnixMilli(),
|
|
})
|
|
}
|
|
|
|
// ========== GET /api/v1/sessions?user_id=xxx — 获取用户会话列表 ==========
|
|
|
|
// List 获取会话列表 (按 updated_at DESC 排序)
|
|
func (h *SessionHandler) List(c *gin.Context) {
|
|
currentUserID := middleware.GetUserID(c)
|
|
isAdmin := middleware.GetIsAdmin(c)
|
|
|
|
// 非管理员只能查询自己的会话
|
|
userID := c.Query("user_id")
|
|
if userID == "" {
|
|
userID = currentUserID
|
|
} else if !isAdmin {
|
|
// 非管理员试图查询其他用户的会话
|
|
if userID != currentUserID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问其他用户的会话"})
|
|
return
|
|
}
|
|
}
|
|
|
|
if h.useDB {
|
|
sessions, err := h.store.GetUserSessions(userID)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 查询会话列表失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询会话失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
// 转换为列表格式
|
|
result := make([]gin.H, 0, len(sessions))
|
|
for _, s := range sessions {
|
|
result = append(result, gin.H{
|
|
"id": s.ID,
|
|
"user_id": s.UserID,
|
|
"title": s.Title,
|
|
"is_main": s.IsMain,
|
|
"created_at": s.CreatedAt.UnixMilli(),
|
|
"updated_at": s.UpdatedAt.UnixMilli(),
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"sessions": result})
|
|
return
|
|
}
|
|
|
|
// 降级:返回空列表
|
|
c.JSON(http.StatusOK, gin.H{"sessions": []gin.H{}})
|
|
}
|
|
|
|
// ========== GET /api/v1/sessions/:id — 获取单个会话 ==========
|
|
|
|
// Get 获取单个会话信息
|
|
func (h *SessionHandler) Get(c *gin.Context) {
|
|
sessionID := c.Param("id")
|
|
|
|
if h.useDB {
|
|
session, err := h.store.GetSession(sessionID)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 查询会话失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询会话失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
if session == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "会话不存在",
|
|
"errorType": "session_not_found",
|
|
"hint": "该会话可能已被删除或尚未创建",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 所有权校验:非管理员只能访问自己的会话
|
|
if !h.canAccess(c, session.UserID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问此会话"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"id": session.ID,
|
|
"user_id": session.UserID,
|
|
"title": session.Title,
|
|
"is_main": session.IsMain,
|
|
"created_at": session.CreatedAt.UnixMilli(),
|
|
"updated_at": session.UpdatedAt.UnixMilli(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "会话存储不可用",
|
|
"errorType": "store_unavailable",
|
|
"hint": "数据库连接未建立,Gateway 运行在仅内存模式",
|
|
})
|
|
}
|
|
|
|
// ========== DELETE /api/v1/sessions/:id — 删除会话 ==========
|
|
|
|
// Delete 删除会话 (不删除记忆)
|
|
func (h *SessionHandler) Delete(c *gin.Context) {
|
|
sessionID := c.Param("id")
|
|
|
|
if h.useDB {
|
|
// 所有权校验:先获取session再验证归属
|
|
session, err := h.store.GetSession(sessionID)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 查询会话失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除会话失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
if session == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "会话不存在",
|
|
"errorType": "session_not_found",
|
|
})
|
|
return
|
|
}
|
|
if !h.canAccess(c, session.UserID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "无权删除此会话"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteSession(sessionID); err != nil {
|
|
log.Printf("[SessionHandler] 删除会话失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除会话失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// 同时清理 Hub 中的缓存
|
|
h.hub.DeleteConversation("", sessionID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
|
}
|
|
|
|
// ========== DELETE /api/v1/sessions?user_id=xxx — 删除用户所有会话 ==========
|
|
|
|
// DeleteAll 删除用户所有会话 (不删除记忆)
|
|
func (h *SessionHandler) DeleteAll(c *gin.Context) {
|
|
currentUserID := middleware.GetUserID(c)
|
|
isAdmin := middleware.GetIsAdmin(c)
|
|
|
|
userID := c.Query("user_id")
|
|
if userID == "" {
|
|
userID = currentUserID
|
|
} else if !isAdmin {
|
|
// 非管理员只能删除自己的会话
|
|
if userID != currentUserID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "无权删除其他用户的会话"})
|
|
return
|
|
}
|
|
}
|
|
|
|
if h.useDB {
|
|
if err := h.store.DeleteAllUserSessions(userID); err != nil {
|
|
log.Printf("[SessionHandler] 删除用户所有会话失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除会话失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
|
}
|
|
|
|
// ========== GET /api/v1/sessions/:id/messages?limit=50 — 获取会话消息 ==========
|
|
|
|
// GetMessages 获取会话的完整消息列表
|
|
func (h *SessionHandler) GetMessages(c *gin.Context) {
|
|
sessionID := c.Param("id")
|
|
|
|
// 所有权校验
|
|
if h.useDB {
|
|
session, err := h.store.GetSession(sessionID)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 查询会话失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
if session == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "会话不存在",
|
|
"errorType": "session_not_found",
|
|
})
|
|
return
|
|
}
|
|
if !h.canAccess(c, session.UserID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问此会话的消息"})
|
|
return
|
|
}
|
|
}
|
|
|
|
limit := 50
|
|
if l := c.Query("limit"); l != "" {
|
|
parsed := 0
|
|
for _, ch := range l {
|
|
if ch < '0' || ch > '9' {
|
|
break
|
|
}
|
|
parsed = parsed*10 + int(ch-'0')
|
|
}
|
|
if parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
offset := 0
|
|
if o := c.Query("offset"); o != "" {
|
|
parsed := 0
|
|
for _, ch := range o {
|
|
if ch < '0' || ch > '9' {
|
|
break
|
|
}
|
|
parsed = parsed*10 + int(ch-'0')
|
|
}
|
|
if parsed > 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
if h.useDB {
|
|
messages, err := h.store.GetMessages(sessionID, limit, offset)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 查询消息失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
// 转换为统一格式
|
|
result := make([]gin.H, 0, len(messages))
|
|
for _, m := range messages {
|
|
result = append(result, gin.H{
|
|
"id": m.ID,
|
|
"session_id": m.SessionID,
|
|
"role": m.Role,
|
|
"content": m.Content,
|
|
"created_at": m.CreatedAt.UnixMilli(),
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"messages": result})
|
|
return
|
|
}
|
|
|
|
// 降级:从 Hub 内存缓存读取
|
|
messages := h.hub.GetConversation("", sessionID)
|
|
if messages == nil {
|
|
messages = []ws.Message{}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"messages": messages})
|
|
}
|
|
|
|
// ========== DELETE /api/v1/sessions/:id/messages — 清空会话消息 ==========
|
|
|
|
// ClearMessages 清空会话所有消息但不删除会话本身
|
|
func (h *SessionHandler) ClearMessages(c *gin.Context) {
|
|
sessionID := c.Param("id")
|
|
|
|
if h.useDB {
|
|
// 所有权校验
|
|
session, err := h.store.GetSession(sessionID)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 查询会话失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空消息失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
if session == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "会话不存在",
|
|
"errorType": "session_not_found",
|
|
})
|
|
return
|
|
}
|
|
if !h.canAccess(c, session.UserID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "无权清空此会话的消息"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.ClearSessionMessages(sessionID); err != nil {
|
|
log.Printf("[SessionHandler] 清空消息失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空消息失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// 同时清理 Hub 内存缓存
|
|
h.hub.DeleteConversation("", sessionID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "cleared"})
|
|
}
|
|
|
|
// ========== Admin 端点 ==========
|
|
|
|
// ListActiveSessions 获取当前所有活跃 WebSocket 会话列表 (管理员)
|
|
func (h *SessionHandler) ListActiveSessions(c *gin.Context) {
|
|
sessions := h.hub.GetAllActiveSessions()
|
|
if sessions == nil {
|
|
sessions = []*ws.SessionState{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"sessions": sessions,
|
|
"total": len(sessions),
|
|
})
|
|
}
|
|
|
|
// GetActiveSessions 返回按用户分组的活跃会话列表
|
|
func (h *SessionHandler) GetActiveSessions(c *gin.Context) {
|
|
sessionsByUser := h.hub.GetActiveSessionsByUser()
|
|
if sessionsByUser == nil {
|
|
sessionsByUser = make(map[string][]*ws.SessionState)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"users": sessionsByUser,
|
|
})
|
|
}
|
|
|
|
// GetSession 获取指定会话的详细信息 (管理员)
|
|
func (h *SessionHandler) GetSession(c *gin.Context) {
|
|
sessionID := c.Param("id")
|
|
if sessionID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少会话ID"})
|
|
return
|
|
}
|
|
|
|
session := h.hub.GetSession(sessionID)
|
|
if session == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "会话不存在",
|
|
"errorType": "session_not_found",
|
|
"hint": "该会话可能已断开,或 Gateway 重启后内存数据已清空",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, session)
|
|
}
|
|
|
|
// ========== GET /api/v1/messages/search?q=xxx&user_id=xxx&limit=50&offset=0 — 全文搜索消息 ==========
|
|
|
|
// SearchMessages 全文搜索消息
|
|
func (h *SessionHandler) SearchMessages(c *gin.Context) {
|
|
query := c.Query("q")
|
|
if query == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少搜索关键词参数 q", "errorType": "missing_query"})
|
|
return
|
|
}
|
|
|
|
userID := c.Query("user_id")
|
|
if userID == "" {
|
|
userID = middleware.GetUserID(c)
|
|
}
|
|
|
|
limit := 50
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
parsed := 0
|
|
for _, ch := range l {
|
|
if ch < '0' || ch > '9' {
|
|
break
|
|
}
|
|
parsed = parsed*10 + int(ch-'0')
|
|
}
|
|
if parsed > 0 && parsed <= 200 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
parsed := 0
|
|
for _, ch := range o {
|
|
if ch < '0' || ch > '9' {
|
|
break
|
|
}
|
|
parsed = parsed*10 + int(ch-'0')
|
|
}
|
|
if parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
if !h.useDB {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"results": []gin.H{},
|
|
"total": 0,
|
|
"query": query,
|
|
})
|
|
return
|
|
}
|
|
|
|
results, total, err := h.store.SearchMessages(userID, query, limit, offset)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 搜索消息失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "搜索失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
|
|
// 转换为统一格式
|
|
items := make([]gin.H, 0, len(results))
|
|
for _, r := range results {
|
|
items = append(items, gin.H{
|
|
"message_id": r.MessageID,
|
|
"session_id": r.SessionID,
|
|
"session_title": r.SessionTitle,
|
|
"role": r.Role,
|
|
"content": r.Content,
|
|
"created_at": r.CreatedAt.UnixMilli(),
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"results": items,
|
|
"total": total,
|
|
"query": query,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
}
|
|
|
|
// ========== GET /api/v1/sessions/:id/export?format=json|markdown|txt — 导出会话 ==========
|
|
|
|
// ExportSession 导出会话为指定格式
|
|
func (h *SessionHandler) ExportSession(c *gin.Context) {
|
|
sessionID := c.Param("id")
|
|
format := c.Query("format")
|
|
if format == "" {
|
|
format = "json"
|
|
}
|
|
|
|
// 验证格式
|
|
switch format {
|
|
case "json", "markdown", "txt":
|
|
// valid
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "不支持的导出格式",
|
|
"errorType": "invalid_format",
|
|
"hint": "支持的格式: json, markdown, txt",
|
|
})
|
|
return
|
|
}
|
|
|
|
if !h.useDB {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "会话存储不可用",
|
|
"errorType": "store_unavailable",
|
|
"hint": "数据库连接未建立,无法导出会话",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 获取会话信息
|
|
session, err := h.store.GetSession(sessionID)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 查询会话失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询会话失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
if session == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "会话不存在",
|
|
"errorType": "session_not_found",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 所有权校验:非管理员只能导出自己的会话
|
|
if !h.canAccess(c, session.UserID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "无权导出此会话"})
|
|
return
|
|
}
|
|
|
|
// 获取所有消息 (不限制数量,导出全部)
|
|
messages, err := h.store.GetMessages(sessionID, 0, 0)
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] 查询消息失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败", "errorType": "db_error"})
|
|
return
|
|
}
|
|
if messages == nil {
|
|
messages = []store.Message{}
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
switch format {
|
|
case "json":
|
|
h.exportJSON(c, session, messages, now)
|
|
case "markdown":
|
|
h.exportMarkdown(c, session, messages, now)
|
|
case "txt":
|
|
h.exportTXT(c, session, messages, now)
|
|
}
|
|
}
|
|
|
|
// exportJSON 导出 JSON 格式
|
|
func (h *SessionHandler) exportJSON(c *gin.Context, session *store.Session, messages []store.Message, now time.Time) {
|
|
type msgOut struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
}
|
|
|
|
type sessionOut struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
type export struct {
|
|
Session sessionOut `json:"session"`
|
|
Messages []msgOut `json:"messages"`
|
|
}
|
|
|
|
msgs := make([]msgOut, 0, len(messages))
|
|
for _, m := range messages {
|
|
msgs = append(msgs, msgOut{
|
|
Role: m.Role,
|
|
Content: m.Content,
|
|
CreatedAt: m.CreatedAt.UnixMilli(),
|
|
})
|
|
}
|
|
|
|
data := export{
|
|
Session: sessionOut{
|
|
ID: session.ID,
|
|
Title: session.Title,
|
|
CreatedAt: session.CreatedAt.UnixMilli(),
|
|
UpdatedAt: session.UpdatedAt.UnixMilli(),
|
|
},
|
|
Messages: msgs,
|
|
}
|
|
|
|
jsonBytes, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
log.Printf("[SessionHandler] JSON序列化失败: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "导出失败", "errorType": "serialization_error"})
|
|
return
|
|
}
|
|
|
|
c.Header("Content-Type", "application/json; charset=utf-8")
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="session_%s.json"`, session.ID))
|
|
c.Data(http.StatusOK, "application/json; charset=utf-8", jsonBytes)
|
|
}
|
|
|
|
// exportMarkdown 导出 Markdown 格式
|
|
func (h *SessionHandler) exportMarkdown(c *gin.Context, session *store.Session, messages []store.Message, now time.Time) {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(fmt.Sprintf("# 对话导出: %s\n", session.Title))
|
|
sb.WriteString(fmt.Sprintf("**会话 ID**: %s\n", session.ID))
|
|
sb.WriteString(fmt.Sprintf("**导出时间**: %s\n", now.Format("2006-01-02 15:04:05")))
|
|
sb.WriteString(fmt.Sprintf("**消息数量**: %d\n", len(messages)))
|
|
sb.WriteString("\n---\n\n")
|
|
|
|
for _, m := range messages {
|
|
timeStr := m.CreatedAt.Format("2006-01-02 15:04:05")
|
|
switch m.Role {
|
|
case "user":
|
|
sb.WriteString(fmt.Sprintf("### 👤 用户 (%s)\n\n", timeStr))
|
|
case "assistant":
|
|
sb.WriteString(fmt.Sprintf("### 🤖 昔涟 (%s)\n\n", timeStr))
|
|
case "system":
|
|
sb.WriteString(fmt.Sprintf("### ⚙️ 系统 (%s)\n\n", timeStr))
|
|
default:
|
|
sb.WriteString(fmt.Sprintf("### %s (%s)\n\n", m.Role, timeStr))
|
|
}
|
|
sb.WriteString(m.Content)
|
|
sb.WriteString("\n\n---\n\n")
|
|
}
|
|
|
|
content := sb.String()
|
|
c.Header("Content-Type", "text/markdown; charset=utf-8")
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="session_%s.md"`, session.ID))
|
|
c.Data(http.StatusOK, "text/markdown; charset=utf-8", []byte(content))
|
|
}
|
|
|
|
// exportTXT 导出纯文本格式
|
|
func (h *SessionHandler) exportTXT(c *gin.Context, session *store.Session, messages []store.Message, now time.Time) {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(fmt.Sprintf("对话导出: %s\n", session.Title))
|
|
sb.WriteString(fmt.Sprintf("会话 ID: %s\n", session.ID))
|
|
sb.WriteString(fmt.Sprintf("导出时间: %s\n", now.Format("2006-01-02 15:04:05")))
|
|
sb.WriteString(fmt.Sprintf("消息数量: %d\n", len(messages)))
|
|
sb.WriteString(strings.Repeat("=", 50))
|
|
sb.WriteString("\n\n")
|
|
|
|
for _, m := range messages {
|
|
timeStr := m.CreatedAt.Format("2006-01-02 15:04:05")
|
|
roleLabel := m.Role
|
|
switch m.Role {
|
|
case "user":
|
|
roleLabel = "用户"
|
|
case "assistant":
|
|
roleLabel = "昔涟"
|
|
case "system":
|
|
roleLabel = "系统"
|
|
}
|
|
sb.WriteString(fmt.Sprintf("[%s] %s:\n%s\n\n", timeStr, roleLabel, m.Content))
|
|
}
|
|
|
|
content := sb.String()
|
|
c.Header("Content-Type", "text/plain; charset=utf-8")
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="session_%s.txt"`, session.ID))
|
|
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(content))
|
|
}
|
|
|
|
// 简单的工具函数
|
|
func randomID(n int) string {
|
|
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
// fallback to deterministic IDs only if crypto/rand fails
|
|
for i := range b {
|
|
b[i] = letters[i%len(letters)]
|
|
}
|
|
return string(b)
|
|
}
|
|
for i := range b {
|
|
b[i] = letters[int(b[i])%len(letters)]
|
|
}
|
|
return string(b)
|
|
}
|