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(`` + alt + ``) + 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 集成指南 -

-
- -
-

📚 更多资源

-

- 查看 - GitHub 项目 - 获取完整文档和使用指南。 -

-

- 如果你有任何问题或建议,欢迎通过 - Discussions - 进行交流。 -

-
-
- - - - \ 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 集成指南 +

+
+ +
+

📚 更多资源

+

+ 查看 + GitHub 项目 + 获取完整文档和使用指南。 +

+

+ 如果你有任何问题或建议,欢迎通过 + Discussions + 进行交流。 +

+
+
+ + + +