feat/Basic Diary

This commit is contained in:
Symb0x76
2025-09-06 23:02:27 +08:00
parent d5f3054d36
commit f75e1c3318
4 changed files with 102 additions and 4 deletions
+74
View File
@@ -13,6 +13,7 @@ import (
"github.com/sjzar/chatlog/internal/chatlog/conf"
"github.com/sjzar/chatlog/internal/errors"
"github.com/sjzar/chatlog/internal/model"
"github.com/sjzar/chatlog/pkg/util"
"github.com/sjzar/chatlog/pkg/version"
)
@@ -24,6 +25,8 @@ func (s *Service) initMCPServer() {
s.mcpServer.AddTool(RecentChatTool, s.handleMCPRecentChat)
s.mcpServer.AddTool(ChatLogTool, s.handleMCPChatLog)
s.mcpServer.AddTool(CurrentTimeTool, s.handleMCPCurrentTime)
// 日记(最近24h我说过话的会话全量消息)
s.mcpServer.AddTool(DiaryTool, s.handleMCPDiary)
s.mcpSSEServer = server.NewSSEServer(s.mcpServer)
s.mcpStreamableServer = server.NewStreamableHTTPServer(s.mcpServer)
}
@@ -154,6 +157,26 @@ var CurrentTimeTool = mcp.NewTool(
注意:此工具不需要任何输入参数,直接调用即可获取当前时间。`),
)
// DiaryTool: 最近24小时内“我”参与(至少有一条我发送的消息)的所有会话的全部消息
var DiaryTool = mcp.NewTool(
"query_diary",
mcp.WithDescription(`查询最近24小时内用户参与过(至少有一条由“我”发送)的所有会话的全部消息,并按会话分组返回。适用于:
1. 让助手总结我一天聊了什么
2. 统计我今天在不同群/联系人中的发言
3. 回顾我参与过的讨论上下文
无任何参数。返回格式:
若存在会话:
[会话显示名(UserName)] - N条
2025-04-18 09:30:05 我 内容
2025-04-18 09:30:15 张三 内容
...
-----------------------------
若无结果:输出"最近24小时没有我参与的会话"。
注意:此工具返回的是完整原始消息内容(已做必要的占位符文本化),不做额外摘要。若需要摘要,请先调用本工具获取原始数据后再进行总结。`),
)
type ContactRequest struct {
Keyword string `json:"keyword"`
Limit int `json:"limit"`
@@ -326,3 +349,54 @@ func (s *Service) handleMCPCurrentTime(ctx context.Context, request mcp.CallTool
},
}, nil
}
// handleMCPDiary 实现 DiaryTool 逻辑
func (s *Service) handleMCPDiary(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
end := time.Now()
start := end.Add(-24 * time.Hour)
sessionsResp, err := s.db.GetSessions("", 0, 0)
if err != nil {
return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "获取会话失败: " + err.Error()}}}, nil
}
type grouped struct {
Talker string
TalkerName string
Messages []*model.Message
}
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})
}
buf := &bytes.Buffer{}
if len(groups) == 0 {
buf.WriteString("最近24小时没有我参与的会话")
} else {
for _, g := range groups {
header := g.Talker
if g.TalkerName != "" { header = fmt.Sprintf("%s(%s)", g.TalkerName, g.Talker) }
buf.WriteString(fmt.Sprintf("[%s] - %d条\n", header, len(g.Messages)))
for _, m := range g.Messages {
sender := m.Sender
if m.IsSelf { sender = "我" }
if m.SenderName != "" { sender = fmt.Sprintf("%s(%s)", m.SenderName, sender) }
buf.WriteString(m.Time.Format("2006-01-02 15:04:05"))
buf.WriteString(" ")
buf.WriteString(sender)
buf.WriteString(" ")
buf.WriteString(m.PlainTextContent())
buf.WriteString("\n")
}
buf.WriteString("-----------------------------\n")
}
}
return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: buf.String()}}}, nil
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -381,7 +381,7 @@
<div class="tab-content" id="diary-tab">
<div class="api-description">
<p>
查询最近24小时内“我”发送的消息按会话分组。<span
查询最近24小时我参与过的会话全部消息。<span
class="badge"
>GET /api/v1/diary</span
>
+25 -1
View File
@@ -3,9 +3,9 @@ package model
import (
"encoding/xml"
"fmt"
"strconv"
"strings"
"time"
"strconv"
"github.com/sjzar/chatlog/pkg/util"
)
@@ -193,6 +193,16 @@ func (m *Message) ParseMediaInfo(data string) error {
// 文件
m.Contents["title"] = msg.App.Title
m.Contents["md5"] = msg.App.MD5
case MessageSubTypeGIF:
// 分享的 GIF/表情(AppMsg type=8),尽量取 URL 作为 cdnurl 用于后续渲染
if msg.App.URL != "" {
m.Contents["cdnurl"] = msg.App.URL
} else if msg.Emoji.CdnURL != "" { // 兜底:有些场景仍可能走 emoji 结构
m.Contents["cdnurl"] = msg.Emoji.CdnURL
}
if msg.App.Title != "" { // 可能包含表情描述
m.Contents["title"] = msg.App.Title
}
case MessageSubTypeMergeForward, MessageSubTypeNote, MessageSubTypeChatRoomNotice:
// 合并转发 & 笔记
m.Contents["title"] = msg.App.Title
@@ -375,6 +385,15 @@ func (m *Message) PlainTextContent() string {
if durStr != "" {
// 规范成整数秒显示
if strings.Contains(durStr, ".") { if f,err:=strconv.ParseFloat(durStr,64); err==nil { durStr = fmt.Sprint(int(f+0.5)) } }
// >=60s 转换为 XmYYs 形式
if secInt, err := strconv.Atoi(durStr); err == nil {
if secInt >= 60 {
min := secInt / 60
sec := secInt % 60
fmtDur := fmt.Sprintf("%dm%02ds", min, sec)
return fmt.Sprintf("[语音(%s)](http://%s/voice/%s)", fmtDur, m.Contents["host"], voice)
}
}
return fmt.Sprintf("[语音(%ss)](http://%s/voice/%s)", durStr, m.Contents["host"], voice)
}
return fmt.Sprintf("[语音](http://%s/voice/%s)", m.Contents["host"], voice)
@@ -426,6 +445,11 @@ func (m *Message) PlainTextContent() string {
case MessageSubTypeFile:
return fmt.Sprintf("[文件|%s](http://%s/file/%s)", m.Contents["title"], m.Contents["host"], m.Contents["md5"])
case MessageSubTypeGIF:
if m.Contents["cdnurl"] != nil {
if u, ok := m.Contents["cdnurl"].(string); ok && strings.HasPrefix(u, "http") {
return fmt.Sprintf("![GIF表情](%s)", u)
}
}
return "[GIF表情]"
case MessageSubTypeMergeForward:
_recordInfo, ok := m.Contents["recordInfo"]