From 1cf5f3179e97784eb22b4e9a0bb16804346f5f99 Mon Sep 17 00:00:00 2001
From: Symb0x76 <9667434@qq.com>
Date: Tue, 2 Sep 2025 16:33:20 +0800
Subject: [PATCH] Feature - All chatrooms output - HTML output with img reder
---
internal/chatlog/http/route.go | 153 ++-
internal/chatlog/http/static/index.htm | 1535 +++++++++++++-----------
2 files changed, 963 insertions(+), 725 deletions(-)
diff --git a/internal/chatlog/http/route.go b/internal/chatlog/http/route.go
index f05402d..88ee7a0 100644
--- a/internal/chatlog/http/route.go
+++ b/internal/chatlog/http/route.go
@@ -4,15 +4,18 @@ import (
"embed"
"encoding/csv"
"fmt"
+ "html/template"
"io/fs"
"net/http"
"os"
"path/filepath"
+ "regexp"
"strings"
"github.com/gin-gonic/gin"
"github.com/sjzar/chatlog/internal/errors"
+ "github.com/sjzar/chatlog/internal/model"
"github.com/sjzar/chatlog/pkg/util"
"github.com/sjzar/chatlog/pkg/util/dat2img"
"github.com/sjzar/chatlog/pkg/util/silk"
@@ -117,6 +120,106 @@ func (s *Service) handleChatlog(c *gin.Context) {
q.Offset = 0
}
+ // 当未提供 talker 时,返回所有会话内消息并按 chatroom(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
+ }
+ g := &grouped{Talker: sess.UserName, TalkerName: sess.NickName, Messages: msgs}
+ groups = append(groups, g)
+ }
+
+ 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("")
+ 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 {
+ 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.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 {
+ 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()
+ }
+ }
+ 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)
@@ -124,7 +227,27 @@ func (s *Service) handleChatlog(c *gin.Context) {
}
switch strings.ToLower(q.Format) {
- case "csv":
+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")
@@ -138,15 +261,12 @@ func (s *Service) handleChatlog(c *gin.Context) {
}
csvWriter.Flush()
case "json":
- // json
c.JSON(http.StatusOK, messages)
default:
- // plain text
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")
@@ -421,3 +541,28 @@ func (s *Service) HandleVoice(c *gin.Context, data []byte) {
}
c.Data(http.StatusOK, "audio/mp3", out)
}
+
+// Markdown 图片语法转换为
,其余文本保持转义
+var mdImgPattern = regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`)
+
+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(`
`)
+ last = m[1]
+ }
+ b.WriteString(template.HTMLEscapeString(src[last:]))
+ return b.String()
+}
diff --git a/internal/chatlog/http/static/index.htm b/internal/chatlog/http/static/index.htm
index 7a53ed1..5e3904c 100644
--- a/internal/chatlog/http/static/index.htm
+++ b/internal/chatlog/http/static/index.htm
@@ -1,757 +1,850 @@
-
-
-
- Chatlog
-
-
-
-
-
-
🎉 恭喜!Chatlog 服务已成功启动
-
- Chatlog 是一个帮助你轻松使用自己聊天数据的工具,现在你可以通过 HTTP
- API 访问你的聊天记录、联系人和群聊信息。
-
-
-
-
-
🔍 API 接口与调试
-
-
-
-
最近会话
-
群聊
-
联系人
-
聊天记录
-
-
-
-
-
-
- 查询最近会话列表。GET /api/v1/session
-
-
-
-
-
-
-
-
-
-
-
-
- 查询群聊列表,可选择性地按关键词搜索。GET /api/v1/chatroom
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 查询联系人列表,可选择性地按关键词搜索。GET /api/v1/contact
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 查询指定时间范围内与特定联系人或群聊的聊天记录。GET /api/v1/chatlog
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🤖 MCP 集成
-
- Chatlog 支持 MCP (Model Context Protocol) SSE 协议,可与支持 MCP 的 AI
- 助手无缝集成。
-
-
SSE 端点:/sse
-
- 详细集成指南请参考
- MCP 集成指南
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ .tab-content {
+ display: none;
+ padding: 20px 0;
+ }
+
+ .tab-content.active {
+ display: block;
+ animation: fadeIn 0.3s;
+ }
+
+ @keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+
+ .button-group {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 20px;
+ }
+
+ .copy-button {
+ background-color: var(--success-color);
+ padding: 8px 15px;
+ font-size: 14px;
+ margin-left: 10px;
+ }
+
+ .copy-button:hover {
+ background-color: var(--success-dark);
+ }
+
+ .error-message {
+ color: var(--error-color);
+ font-weight: bold;
+ margin-top: 10px;
+ padding: 10px;
+ border-radius: 4px;
+ background-color: rgba(231, 76, 60, 0.1);
+ border-left: 4px solid var(--error-color);
+ display: none;
+ }
+
+ .api-description {
+ margin-bottom: 15px;
+ color: #555;
+ }
+
+ .badge {
+ display: inline-block;
+ padding: 3px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 600;
+ margin-left: 8px;
+ background-color: rgba(52, 152, 219, 0.1);
+ color: var(--primary-color);
+ }
+
+ .optional-param {
+ font-size: 12px;
+ color: #777;
+ margin-left: 5px;
+ font-style: italic;
+ }
+
+ .required-field {
+ color: var(--error-color);
+ font-weight: bold;
+ }
+
+
+
+
+
+
🎉 恭喜!Chatlog 服务已成功启动
+
+ Chatlog
+ 是一个帮助你轻松使用自己聊天数据的工具,现在你可以通过 HTTP
+ API 访问你的聊天记录、联系人和群聊信息。
+
+
+
+
+
🔍 API 接口与调试
+
+
+
+
+ 最近会话
+
+
群聊
+
联系人
+
聊天记录
+
+
+
+
+
+
+ 查询最近会话列表。GET /api/v1/session
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询群聊列表,可选择性地按关键词搜索。GET /api/v1/chatroom
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询联系人列表,可选择性地按关键词搜索。GET /api/v1/contact
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询指定时间范围内与特定联系人或群聊的聊天记录。GET /api/v1/chatlog
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🤖 MCP 集成
+
+ Chatlog 支持 MCP (Model Context Protocol) SSE 协议,可与支持
+ MCP 的 AI 助手无缝集成。
+
+
SSE 端点:/sse
+
+ 详细集成指南请参考
+ MCP 集成指南
+
+
+
+
+
+
+
+
+