diff --git a/internal/chatlog/http/route.go b/internal/chatlog/http/route.go index 88ee7a0..04a08c0 100644 --- a/internal/chatlog/http/route.go +++ b/internal/chatlog/http/route.go @@ -11,6 +11,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "github.com/gin-gonic/gin" @@ -21,11 +22,15 @@ import ( "github.com/sjzar/chatlog/pkg/util/silk" ) -// EFS holds embedded file system data for static assets. -// //go:embed static var EFS embed.FS +// 统一的 HTML 预览组件片段 +var previewHTMLSnippet = ` + +
预览
+` + func (s *Service) initRouter() { s.initBaseRouter() s.initMediaRouter() @@ -35,16 +40,19 @@ func (s *Service) initRouter() { func (s *Service) initBaseRouter() { staticDir, _ := fs.Sub(EFS, "static") - s.router.StaticFS("/static", http.FS(staticDir)) s.router.StaticFileFS("/favicon.ico", "./favicon.ico", http.FS(staticDir)) s.router.StaticFileFS("/", "./index.htm", http.FS(staticDir)) - - s.router.GET("/health", func(ctx *gin.Context) { - ctx.JSON(http.StatusOK, gin.H{"status": "ok"}) + s.router.GET("/health", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"status":"ok"}) }) + s.router.NoRoute(func(c *gin.Context){ + path := c.Request.URL.Path + if strings.HasPrefix(path,"/api") || strings.HasPrefix(path,"/static") { + c.JSON(http.StatusNotFound, gin.H{"error":"Not found"}) + return + } + c.Header("Cache-Control","no-cache, no-store, max-age=0, must-revalidate, value") + c.Redirect(http.StatusFound, "/") }) - - s.router.NoRoute(s.NoRoute) } func (s *Service) initMediaRouter() { @@ -62,216 +70,142 @@ func (s *Service) initAPIRouter() { api.GET("/contact", s.handleContacts) api.GET("/chatroom", s.handleChatRooms) api.GET("/session", s.handleSessions) + api.GET("/diary", s.handleDiary) } } func (s *Service) initMCPRouter() { - s.router.Any("/mcp", func(c *gin.Context) { - s.mcpStreamableServer.ServeHTTP(c.Writer, c.Request) - }) - s.router.Any("/sse", func(c *gin.Context) { - s.mcpSSEServer.ServeHTTP(c.Writer, c.Request) - }) - s.router.Any("/message", func(c *gin.Context) { - s.mcpSSEServer.ServeHTTP(c.Writer, c.Request) - }) -} - -// NoRoute handles 404 Not Found errors. If the request URL starts with "/api" -// or "/static", it responds with a JSON error. Otherwise, it redirects to the root path. -func (s *Service) NoRoute(c *gin.Context) { - path := c.Request.URL.Path - switch { - case strings.HasPrefix(path, "/api"), strings.HasPrefix(path, "/static"): - c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) - default: - c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") - c.Redirect(http.StatusFound, "/") - } + s.router.Any("/mcp", func(c *gin.Context) { s.mcpStreamableServer.ServeHTTP(c.Writer, c.Request) }) + s.router.Any("/sse", func(c *gin.Context) { s.mcpSSEServer.ServeHTTP(c.Writer, c.Request) }) + s.router.Any("/message", func(c *gin.Context) { s.mcpSSEServer.ServeHTTP(c.Writer, c.Request) }) } func (s *Service) handleChatlog(c *gin.Context) { + q := struct { + Time string `form:"time"` + Talker string `form:"talker"` + Sender string `form:"sender"` + Keyword string `form:"keyword"` + Limit int `form:"limit"` + Offset int `form:"offset"` + Format string `form:"format"` + }{} - q := struct { - Time string `form:"time"` - Talker string `form:"talker"` - Sender string `form:"sender"` - Keyword string `form:"keyword"` - Limit int `form:"limit"` - Offset int `form:"offset"` - Format string `form:"format"` - }{} + if err := c.BindQuery(&q); err != nil { errors.Err(c, err); return } - if err := c.BindQuery(&q); err != nil { - errors.Err(c, err) - return - } + start, end, ok := util.TimeRangeOf(q.Time) + if !ok { errors.Err(c, errors.InvalidArg("time")) } + if q.Limit < 0 { q.Limit = 0 } + if q.Offset < 0 { q.Offset = 0 } - var err error - start, end, ok := util.TimeRangeOf(q.Time) - if !ok { - errors.Err(c, errors.InvalidArg("time")) - } - if q.Limit < 0 { - q.Limit = 0 - } - - if q.Offset < 0 { - q.Offset = 0 - } - - // 当未提供 talker 时,返回所有会话内消息并按 chatroom(talker) 分组 - if q.Talker == "" { - // 获取所有会话(不分页) - sessionsResp, err := s.db.GetSessions("", 0, 0) - if err != nil { - errors.Err(c, err) + // 1. 未指定 talker: 分组输出 + if q.Talker == "" { + sessionsResp, err := s.db.GetSessions("", 0, 0) + if err != nil { errors.Err(c, err); return } + type grouped struct { + Talker string `json:"talker"` + TalkerName string `json:"talkerName,omitempty"` + Messages []*model.Message `json:"messages"` + } + groups := make([]*grouped,0) + for _, sess := range sessionsResp.Items { + msgs, err := s.db.GetMessages(start, end, sess.UserName, q.Sender, q.Keyword, 0, 0) + if err != nil || len(msgs)==0 { continue } + groups = append(groups, &grouped{Talker:sess.UserName, TalkerName:sess.NickName, Messages:msgs}) + } + switch strings.ToLower(q.Format) { + case "html": + c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + c.Writer.WriteString("Chatlog") + c.Writer.WriteString(fmt.Sprintf("

All Messages %s ~ %s

", start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"))) + for _, g := range groups { + title := g.Talker + if g.TalkerName != "" { title = fmt.Sprintf("%s (%s)", g.TalkerName, g.Talker) } + c.Writer.WriteString("
" + template.HTMLEscapeString(title) + fmt.Sprintf(" - %d 条消息", len(g.Messages))) + for _, m := range g.Messages { + m.SetContent("host", c.Request.Host) + senderDisplay := m.Sender + if m.IsSelf { senderDisplay = "我" } + if m.SenderName != "" { senderDisplay = template.HTMLEscapeString(m.SenderName) + "(" + template.HTMLEscapeString(senderDisplay) + ")" } else { senderDisplay = template.HTMLEscapeString(senderDisplay) } + c.Writer.WriteString("
"+ senderDisplay +""+ m.Time.Format("2006-01-02 15:04:05") +"
"+ messageHTMLPlaceholder(m) +"
") + } + c.Writer.WriteString("
") + } + c.Writer.WriteString(previewHTMLSnippet) + c.Writer.WriteString("") + case "json": + c.JSON(http.StatusOK, groups) + case "csv": + c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8") + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=all_%s_%s.csv", start.Format("2006-01-02"), end.Format("2006-01-02"))) + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Flush() + csvWriter := csv.NewWriter(c.Writer) + csvWriter.Write([]string{"Talker","TalkerName","Time","SenderName","Sender","Content"}) + for _, g := range groups { for _, m := range g.Messages { csvWriter.Write([]string{g.Talker, g.TalkerName, m.Time.Format("2006-01-02 15:04:05"), m.SenderName, m.Sender, m.PlainTextContent()}) } } + csvWriter.Flush() + default: + c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Flush() + for _, g := range groups { + header := g.Talker + if g.TalkerName!="" { header = fmt.Sprintf("%s (%s)", g.TalkerName, g.Talker) } + c.Writer.WriteString(header+"\n") + for _, m := range g.Messages { + sender := m.Sender + if m.IsSelf { sender = "我" } + if m.SenderName!="" { sender = m.SenderName + "("+sender+")" } + c.Writer.WriteString(m.Time.Format("2006-01-02 15:04:05")+" "+sender+" "+m.PlainTextContent()+"\n") + } + c.Writer.WriteString("-----------------------------\n") + } + } return } - // 结果分组结构 - type grouped struct { - Talker string `json:"talker"` - TalkerName string `json:"talkerName,omitempty"` - Messages []*model.Message `json:"messages"` - } - - groups := make([]*grouped, 0) - - // 遍历每个会话获取消息 - for _, sess := range sessionsResp.Items { - msgs, err := s.db.GetMessages(start, end, sess.UserName, q.Sender, q.Keyword, 0, 0) - if err != nil || len(msgs) == 0 { - continue - } - g := &grouped{Talker: sess.UserName, TalkerName: sess.NickName, Messages: msgs} - groups = append(groups, g) - } - + // 2. 指定 talker: 单会话消息 + messages, err := s.db.GetMessages(start, end, q.Talker, q.Sender, q.Keyword, q.Limit, q.Offset) + if err != nil { errors.Err(c, err); return } switch strings.ToLower(q.Format) { case "html": - // HTML 输出:分组折叠 c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - c.Writer.WriteString("Chatlog") - c.Writer.WriteString(fmt.Sprintf("

All Messages %s ~ %s

", start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"))) - for _, g := range groups { - title := g.Talker - if g.TalkerName != "" { - title = fmt.Sprintf("%s (%s)", g.TalkerName, g.Talker) - } - c.Writer.WriteString("
" + template.HTMLEscapeString(title) + fmt.Sprintf(" - %d 条消息", len(g.Messages))) - for _, m := range g.Messages { - m.SetContent("host", c.Request.Host) - c.Writer.WriteString("
") - c.Writer.WriteString("
") - c.Writer.WriteString(template.HTMLEscapeString(m.SenderName)) - if m.SenderName != "" { c.Writer.WriteString("(") } - c.Writer.WriteString(template.HTMLEscapeString(m.Sender)) - if m.SenderName != "" { c.Writer.WriteString(")") } - c.Writer.WriteString(" ") - c.Writer.WriteString(m.Time.Format("2006-01-02 15:04:05")) - c.Writer.WriteString("
") - c.Writer.WriteString("
")
-					c.Writer.WriteString(renderMarkdownImages(m.PlainTextContent()))
-					c.Writer.WriteString("
") - } - c.Writer.WriteString("
") + c.Writer.WriteString("Chatlog") + c.Writer.WriteString(fmt.Sprintf("

Messages %s ~ %s (%s)

", start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"), template.HTMLEscapeString(q.Talker))) + for _, m := range messages { + m.SetContent("host", c.Request.Host) + c.Writer.WriteString("
") + if m.SenderName != "" { c.Writer.WriteString(template.HTMLEscapeString(m.SenderName)+"(") } + c.Writer.WriteString(template.HTMLEscapeString(m.Sender)) + if m.SenderName != "" { c.Writer.WriteString(")") } + c.Writer.WriteString(""+m.Time.Format("2006-01-02 15:04:05")+"
")
+				c.Writer.WriteString(messageHTMLPlaceholder(m))
+				c.Writer.WriteString("
") } + c.Writer.WriteString(previewHTMLSnippet) c.Writer.WriteString("") - case "json": - c.JSON(http.StatusOK, groups) case "csv": c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8") - c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=all_%s_%s.csv", start.Format("2006-01-02"), end.Format("2006-01-02"))) + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s_%s_%s.csv", q.Talker, start.Format("2006-01-02"), end.Format("2006-01-02"))) c.Writer.Header().Set("Cache-Control", "no-cache") c.Writer.Header().Set("Connection", "keep-alive") c.Writer.Flush() csvWriter := csv.NewWriter(c.Writer) - csvWriter.Write([]string{"Talker", "TalkerName", "Time", "SenderName", "Sender", "Content"}) - for _, g := range groups { - for _, m := range g.Messages { - row := m.CSV(c.Request.Host) - // 原 CSV: Time, SenderName, Sender, TalkerName, Talker, Content - csvWriter.Write([]string{m.Talker, m.TalkerName, row[0], row[1], row[2], row[5]}) - } - } + csvWriter.Write([]string{"Time","SenderName","Sender","TalkerName","Talker","Content"}) + for _, m := range messages { csvWriter.Write(m.CSV(c.Request.Host)) } csvWriter.Flush() + case "json": + c.JSON(http.StatusOK, messages) default: c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8") c.Writer.Header().Set("Cache-Control", "no-cache") c.Writer.Header().Set("Connection", "keep-alive") c.Writer.Flush() - for _, g := range groups { - c.Writer.WriteString("====================\n") - if g.TalkerName != "" { - c.Writer.WriteString(fmt.Sprintf("%s (%s)\n", g.TalkerName, g.Talker)) - } else { - c.Writer.WriteString(fmt.Sprintf("%s\n", g.Talker)) - } - c.Writer.WriteString("--------------------\n") - for _, m := range g.Messages { - c.Writer.WriteString(m.PlainText(true, util.PerfectTimeFormat(start, end), c.Request.Host)) - c.Writer.WriteString("\n") - } - c.Writer.Flush() + for _, m := range messages { + c.Writer.WriteString(m.PlainText(strings.Contains(q.Talker, ","), util.PerfectTimeFormat(start, end), c.Request.Host)+"\n") } - } - return - } - - // 正常:指定 talker - messages, err := s.db.GetMessages(start, end, q.Talker, q.Sender, q.Keyword, q.Limit, q.Offset) - if err != nil { - errors.Err(c, err) - return - } - - switch strings.ToLower(q.Format) { -case "html": - c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - c.Writer.WriteString("Chatlog") - c.Writer.WriteString(fmt.Sprintf("

Messages %s ~ %s (%s)

", start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"), template.HTMLEscapeString(q.Talker))) - for _, m := range messages { - m.SetContent("host", c.Request.Host) - c.Writer.WriteString("
") - c.Writer.WriteString("
") - c.Writer.WriteString(template.HTMLEscapeString(m.SenderName)) - if m.SenderName != "" { c.Writer.WriteString("(") } - c.Writer.WriteString(template.HTMLEscapeString(m.Sender)) - if m.SenderName != "" { c.Writer.WriteString(")") } - c.Writer.WriteString("") - c.Writer.WriteString(m.Time.Format("2006-01-02 15:04:05")) - c.Writer.WriteString("
") - c.Writer.WriteString("
")
-		c.Writer.WriteString(renderMarkdownImages(m.PlainTextContent()))
-		c.Writer.WriteString("
") - } - c.Writer.WriteString("") -case "csv": - c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8") - c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s_%s_%s.csv", q.Talker, start.Format("2006-01-02"), end.Format("2006-01-02"))) - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Flush() - - csvWriter := csv.NewWriter(c.Writer) - csvWriter.Write([]string{"Time", "SenderName", "Sender", "TalkerName", "Talker", "Content"}) - for _, m := range messages { - csvWriter.Write(m.CSV(c.Request.Host)) - } - csvWriter.Flush() - case "json": - c.JSON(http.StatusOK, messages) - default: - c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8") - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Flush() - for _, m := range messages { - c.Writer.WriteString(m.PlainText(strings.Contains(q.Talker, ","), util.PerfectTimeFormat(start, end), c.Request.Host)) - c.Writer.WriteString("\n") - c.Writer.Flush() - } } } @@ -385,6 +319,33 @@ func (s *Service) handleSessions(c *gin.Context) { } format := strings.ToLower(q.Format) switch format { + case "html": + c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + c.Writer.WriteHeader(http.StatusOK) + c.Writer.Write([]byte(`
`)) + for _, session := range sessions.Items { + // 转义 + name := template.HTMLEscapeString(session.NickName) + uname := template.HTMLEscapeString(session.UserName) + content := template.HTMLEscapeString(session.Content) + if len(content) > 400 { // 简单截断,避免过长 + content = content[:400] + "..." + } + content = strings.ReplaceAll(content, "\r", "") + content = strings.ReplaceAll(content, "\n", "\n") // 让 pre-wrap 生效 + c.Writer.Write([]byte(`
` + name + `(` + uname + `)` + session.NTime.Format("2006-01-02 15:04:05") + `
` + content + `
`)) + } + c.Writer.Write([]byte(`
`)) + c.Writer.Write([]byte(previewHTMLSnippet)) + c.Writer.Flush() + return case "csv": c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8") c.Writer.Header().Set("Cache-Control", "no-cache") @@ -412,6 +373,95 @@ func (s *Service) handleSessions(c *gin.Context) { } } +// handleDiary 返回最近24小时内“我”发送的消息,按 talker 分组。 +// GET /api/v1/diary?format=(html|json|csv|text) +func (s *Service) handleDiary(c *gin.Context) { + q := struct { Format string `form:"format"` }{} + if err := c.BindQuery(&q); err != nil { errors.Err(c, err); return } + end := time.Now() + start := end.Add(-24 * time.Hour) + + // 获取所有会话 + sessionsResp, err := s.db.GetSessions("", 0, 0) + if err != nil { errors.Err(c, err); return } + + type grouped struct { + Talker string `json:"talker"` + TalkerName string `json:"talkerName,omitempty"` + Messages []*model.Message `json:"messages"` + } + groups := make([]*grouped,0) + + for _, sess := range sessionsResp.Items { + msgs, err := s.db.GetMessages(start, end, sess.UserName, "", "", 0, 0) + if err != nil || len(msgs)==0 { continue } + hasSelf := false + for _, m := range msgs { if m.IsSelf { hasSelf = true; break } } + if !hasSelf { continue } + groups = append(groups, &grouped{Talker:sess.UserName, TalkerName:sess.NickName, Messages:msgs}) + } + + format := strings.ToLower(q.Format) + switch format { + case "html": + c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + c.Writer.WriteString(`Diary - 24h`) + c.Writer.WriteString(fmt.Sprintf("

最近24小时我参与过的会话全部消息(%s ~ %s)

", start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"))) + for _, g := range groups { + title := g.Talker + if g.TalkerName != "" { title = fmt.Sprintf("%s (%s)", g.TalkerName, g.Talker) } + c.Writer.WriteString("
" + template.HTMLEscapeString(title) + fmt.Sprintf(" - %d 条消息", len(g.Messages))) + for _, m := range g.Messages { + m.SetContent("host", c.Request.Host) + senderDisplay := m.Sender + if m.IsSelf { senderDisplay = "我" } + if m.SenderName != "" { + senderDisplay = template.HTMLEscapeString(m.SenderName) + "(" + template.HTMLEscapeString(senderDisplay) + ")" + } else { + senderDisplay = template.HTMLEscapeString(senderDisplay) + } + c.Writer.WriteString("
" + senderDisplay + "" + m.Time.Format("2006-01-02 15:04:05") + "
" + messageHTMLPlaceholder(m) + "
") + } + c.Writer.WriteString("
") + } + c.Writer.WriteString(previewHTMLSnippet) + c.Writer.WriteString("") + case "json": + c.JSON(http.StatusOK, groups) + case "csv": + c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Flush() + writer := csv.NewWriter(c.Writer) + writer.Write([]string{"Talker","TalkerName","Time","SenderName","Sender","Content"}) + for _, g := range groups { for _, m := range g.Messages { writer.Write([]string{m.Talker, m.TalkerName, m.Time.Format("2006-01-02 15:04:05"), m.SenderName, m.Sender, m.PlainTextContent()}) } } + writer.Flush() + default: + c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Flush() + for _, g := range groups { + if g.TalkerName!="" { c.Writer.WriteString(fmt.Sprintf("%s (%s)\n", g.TalkerName, g.Talker)) } else { c.Writer.WriteString(g.Talker+"\n") } + for _, m := range g.Messages { + senderDisplay := m.Sender + if m.IsSelf { senderDisplay = "我" } + if m.SenderName != "" { + senderDisplay = m.SenderName + "(" + senderDisplay + ")" + } + c.Writer.WriteString(m.Time.Format("2006-01-02 15:04:05")) + c.Writer.WriteString(" ") + c.Writer.WriteString(senderDisplay) + c.Writer.WriteString(" ") + c.Writer.WriteString(m.PlainTextContent()) + c.Writer.WriteString("\n") + } + c.Writer.WriteString("-----------------------------\n") + } + } +} + func (s *Service) handleMedia(c *gin.Context, _type string) { key := strings.TrimPrefix(c.Param("key"), "/") if key == "" { @@ -542,27 +592,37 @@ func (s *Service) HandleVoice(c *gin.Context, data []byte) { c.Data(http.StatusOK, "audio/mp3", out) } -// Markdown 图片语法转换为 ,其余文本保持转义 -var mdImgPattern = regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`) +// 统一占位符:将 PlainTextContent 里形如 ![标签](url) 或 [标签](url) 的模式转成超链接形式,仅显示 [标签]。 +var placeholderPattern = regexp.MustCompile(`!?\[([^\]]+)\]\((https?://[^)]+)\)`) -func renderMarkdownImages(src string) string { - if src == "" { - return "" - } - matches := mdImgPattern.FindAllStringSubmatchIndex(src, -1) - if len(matches) == 0 { - return template.HTMLEscapeString(src) - } - var b strings.Builder - last := 0 - for _, m := range matches { - // m: full, alt, url indices - b.WriteString(template.HTMLEscapeString(src[last:m[0]])) - alt := template.HTMLEscapeString(src[m[2]:m[3]]) - url := template.HTMLEscapeString(src[m[4]:m[5]]) - b.WriteString(`` + alt + ``) - last = m[1] - } - b.WriteString(template.HTMLEscapeString(src[last:])) - return b.String() +func messageHTMLPlaceholder(m *model.Message) string { + content := m.PlainTextContent() + return placeholderPattern.ReplaceAllStringFunc(content, func(s string) string { + matches := placeholderPattern.FindStringSubmatch(s) + if len(matches) != 3 { return template.HTMLEscapeString(s) } + fullLabel := matches[1] + url := matches[2] + left := fullLabel + rest := "" + if p := strings.Index(fullLabel, "|") ; p >= 0 { + left = fullLabel[:p] + rest = fullLabel[p+1:] + } + className := "media" + if left == "动画表情" || left == "GIF表情" || strings.Contains(left, "表情") { + className = "media anim" + } + var anchorText string + if left == "链接" { // 保留完整形式 [链接|标题\n更多说明] + escapedFull := template.HTMLEscapeString(fullLabel) + escapedFull = strings.ReplaceAll(escapedFull, "\r", "") + escapedFull = strings.ReplaceAll(escapedFull, "\n", "
") + anchorText = "[" + escapedFull + "]" + } else if left == "文件" && rest != "" { // 文件保留文件名 + anchorText = "[文件]" + template.HTMLEscapeString(rest) + } else { + anchorText = "[" + template.HTMLEscapeString(left) + "]" + } + return `` + anchorText + `` + }) } diff --git a/internal/chatlog/http/static/index.htm b/internal/chatlog/http/static/index.htm index 5e3904c..483d04a 100644 --- a/internal/chatlog/http/static/index.htm +++ b/internal/chatlog/http/static/index.htm @@ -350,6 +350,7 @@
群聊
联系人
聊天记录
+
日记
@@ -371,6 +372,33 @@ + + + + + + +
+
+

+ 查询最近24小时内“我”发送的消息按会话分组。GET /api/v1/diary +

+
+
+ +
@@ -755,6 +783,15 @@ if (sessionFormat) params.append("format", sessionFormat); break; + case "diary": + url += "diary"; + const diaryFormat = + document.getElementById( + "diary-format" + ).value; + if (diaryFormat) + params.append("format", diaryFormat); + break; } // 添加参数到URL diff --git a/internal/model/message.go b/internal/model/message.go index e2601e0..0cb4da9 100644 --- a/internal/model/message.go +++ b/internal/model/message.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" "time" + "strconv" "github.com/sjzar/chatlog/pkg/util" ) @@ -366,6 +367,16 @@ func (m *Message) PlainTextContent() string { return fmt.Sprintf("![图片](http://%s/image/%s)", m.Contents["host"], strings.Join(keylist, ",")) case MessageTypeVoice: if voice, ok := m.Contents["voice"]; ok { + // 可选时长字段(可能来源于不同表:voicelength/voiceduration/length 秒) + var durStr string + if d, ok2 := m.Contents["voicelength"]; ok2 { durStr = fmt.Sprint(d) } + if d, ok2 := m.Contents["voiceduration"]; ok2 { durStr = fmt.Sprint(d) } + if d, ok2 := m.Contents["length"]; ok2 && durStr=="" { durStr = fmt.Sprint(d) } + if durStr != "" { + // 规范成整数秒显示 + if strings.Contains(durStr, ".") { if f,err:=strconv.ParseFloat(durStr,64); err==nil { durStr = fmt.Sprint(int(f+0.5)) } } + return fmt.Sprintf("[语音(%ss)](http://%s/voice/%s)", durStr, m.Contents["host"], voice) + } return fmt.Sprintf("[语音](http://%s/voice/%s)", m.Contents["host"], voice) } return "[语音]"