Skip to content

Commit f31953c

Browse files
authored
Support Media Message (sjzar#9)
1 parent 98f4145 commit f31953c

File tree

24 files changed

+1428
-136
lines changed

24 files changed

+1428
-136
lines changed

go.mod

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ require (
88
github.com/google/uuid v1.6.0
99
github.com/klauspost/compress v1.18.0
1010
github.com/mattn/go-sqlite3 v1.14.24
11-
github.com/rivo/tview v0.0.0-20250322200051-73a5bd7d6839
11+
github.com/pierrec/lz4/v4 v4.1.22
12+
github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814
1213
github.com/shirou/gopsutil/v4 v4.25.2
1314
github.com/sirupsen/logrus v1.9.3
1415
github.com/spf13/cobra v1.9.1
15-
github.com/spf13/viper v1.20.0
16+
github.com/spf13/viper v1.20.1
1617
golang.org/x/crypto v0.36.0
1718
golang.org/x/sys v0.31.0
18-
google.golang.org/protobuf v1.36.5
19+
google.golang.org/protobuf v1.36.6
1920
howett.net/plist v1.0.1
2021
)
2122

@@ -47,7 +48,7 @@ require (
4748
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
4849
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
4950
github.com/rivo/uniseg v0.4.7 // indirect
50-
github.com/sagikazarmark/locafero v0.8.0 // indirect
51+
github.com/sagikazarmark/locafero v0.9.0 // indirect
5152
github.com/sourcegraph/conc v0.3.0 // indirect
5253
github.com/spf13/afero v1.14.0 // indirect
5354
github.com/spf13/cast v1.7.1 // indirect
@@ -60,7 +61,7 @@ require (
6061
github.com/yusufpapurcu/wmi v1.2.4 // indirect
6162
go.uber.org/multierr v1.11.0 // indirect
6263
golang.org/x/arch v0.15.0 // indirect
63-
golang.org/x/net v0.37.0 // indirect
64+
golang.org/x/net v0.38.0 // indirect
6465
golang.org/x/term v0.30.0 // indirect
6566
golang.org/x/text v0.23.0 // indirect
6667
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,23 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
8181
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
8282
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
8383
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
84+
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
85+
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
8486
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8587
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8688
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
8789
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
88-
github.com/rivo/tview v0.0.0-20250322200051-73a5bd7d6839 h1:/v0ptNHBQaQCxlvS4QLxLKKGfsSA9hcZcNgqVgmPRro=
89-
github.com/rivo/tview v0.0.0-20250322200051-73a5bd7d6839/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
90+
github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814 h1:pJIO3sp+rkDbJTeqqpe2Oihq3hegiM5ASvsd6S0pvjg=
91+
github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
9092
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
9193
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
9294
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
9395
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
9496
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
9597
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
9698
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
97-
github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
98-
github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
99+
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
100+
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
99101
github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
100102
github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
101103
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -110,8 +112,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
110112
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
111113
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
112114
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
113-
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
114-
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
115+
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
116+
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
115117
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
116118
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
117119
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -161,8 +163,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
161163
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
162164
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
163165
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
164-
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
165-
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
166+
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
167+
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
166168
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
167169
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
168170
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -217,8 +219,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
217219
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
218220
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
219221
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
220-
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
221-
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
222+
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
223+
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
222224
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
223225
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
224226
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/chatlog/database/service.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ func (s *Service) GetSessions(key string, limit, offset int) (*wechatdb.GetSessi
5757
return s.db.GetSessions(key, limit, offset)
5858
}
5959

60+
func (s *Service) GetMedia(_type string, key string) (*model.Media, error) {
61+
return s.db.GetMedia(_type, key)
62+
}
63+
6064
// Close closes the database connection
6165
func (s *Service) Close() {
6266
// Add cleanup code if needed

internal/chatlog/http/route.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import (
55
"fmt"
66
"io/fs"
77
"net/http"
8+
"os"
9+
"path/filepath"
810
"strings"
911

1012
"github.com/sjzar/chatlog/internal/errors"
1113
"github.com/sjzar/chatlog/pkg/util"
14+
"github.com/sjzar/chatlog/pkg/util/dat2img"
1215

1316
"github.com/gin-gonic/gin"
1417
)
@@ -27,6 +30,12 @@ func (s *Service) initRouter() {
2730
router.StaticFileFS("/favicon.ico", "./favicon.ico", http.FS(staticDir))
2831
router.StaticFileFS("/", "./index.htm", http.FS(staticDir))
2932

33+
// Media
34+
router.GET("/image/:key", s.GetImage)
35+
router.GET("/video/:key", s.GetVideo)
36+
router.GET("/file/:key", s.GetFile)
37+
router.GET("/data/*path", s.GetMediaData)
38+
3039
// MCP Server
3140
{
3241
router.GET("/sse", s.mcp.HandleSSE)
@@ -108,7 +117,7 @@ func (s *Service) GetChatlog(c *gin.Context) {
108117
c.Writer.Flush()
109118

110119
for _, m := range messages {
111-
c.Writer.WriteString(m.PlainText(len(q.Talker) == 0))
120+
c.Writer.WriteString(m.PlainText(len(q.Talker) == 0, c.Request.Host))
112121
c.Writer.WriteString("\n")
113122
c.Writer.Flush()
114123
}
@@ -251,3 +260,86 @@ func (s *Service) GetSessions(c *gin.Context) {
251260
c.Writer.Flush()
252261
}
253262
}
263+
264+
func (s *Service) GetImage(c *gin.Context) {
265+
s.GetMedia(c, "image")
266+
}
267+
268+
func (s *Service) GetVideo(c *gin.Context) {
269+
s.GetMedia(c, "video")
270+
}
271+
272+
func (s *Service) GetFile(c *gin.Context) {
273+
s.GetMedia(c, "file")
274+
}
275+
276+
func (s *Service) GetMedia(c *gin.Context, _type string) {
277+
key := c.Param("key")
278+
if key == "" {
279+
errors.Err(c, errors.ErrInvalidArg(key))
280+
return
281+
}
282+
283+
media, err := s.db.GetMedia(_type, key)
284+
if err != nil {
285+
errors.Err(c, err)
286+
return
287+
}
288+
289+
if c.Query("info") != "" {
290+
c.JSON(http.StatusOK, media)
291+
return
292+
}
293+
294+
c.Redirect(http.StatusFound, "/data/"+media.Path)
295+
}
296+
297+
func (s *Service) GetMediaData(c *gin.Context) {
298+
relativePath := filepath.Clean(c.Param("path"))
299+
300+
absolutePath := filepath.Join(s.ctx.DataDir, relativePath)
301+
302+
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
303+
c.JSON(http.StatusNotFound, gin.H{
304+
"error": "File not found",
305+
})
306+
return
307+
}
308+
309+
ext := strings.ToLower(filepath.Ext(absolutePath))
310+
switch {
311+
case ext == ".dat":
312+
s.HandleDatFile(c, absolutePath)
313+
default:
314+
// 直接返回文件
315+
c.File(absolutePath)
316+
}
317+
318+
}
319+
320+
func (s *Service) HandleDatFile(c *gin.Context, path string) {
321+
322+
b, err := os.ReadFile(path)
323+
if err != nil {
324+
errors.Err(c, err)
325+
return
326+
}
327+
out, ext, err := dat2img.Dat2Image(b)
328+
if err != nil {
329+
c.File(path)
330+
return
331+
}
332+
333+
switch ext {
334+
case "jpg":
335+
c.Data(http.StatusOK, "image/jpeg", out)
336+
case "png":
337+
c.Data(http.StatusOK, "image/png", out)
338+
case "gif":
339+
c.Data(http.StatusOK, "image/gif", out)
340+
case "bmp":
341+
c.Data(http.StatusOK, "image/bmp", out)
342+
default:
343+
c.File(path)
344+
}
345+
}

internal/chatlog/mcp/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ func (s *Service) toolsCall(session *mcp.Session, req *mcp.Request) error {
200200
return fmt.Errorf("无法获取聊天记录: %v", err)
201201
}
202202
for _, m := range messages {
203-
buf.WriteString(m.PlainText(len(talker) == 0))
203+
buf.WriteString(m.PlainText(len(talker) == 0, ""))
204204
buf.WriteString("\n")
205205
}
206206
default:
@@ -273,7 +273,7 @@ func (s *Service) resourcesRead(session *mcp.Session, req *mcp.Request) error {
273273
return fmt.Errorf("无法获取聊天记录: %v", err)
274274
}
275275
for _, m := range messages {
276-
buf.WriteString(m.PlainText(len(u.Host) == 0))
276+
buf.WriteString(m.PlainText(len(u.Host) == 0, ""))
277277
buf.WriteString("\n")
278278
}
279279
default:

internal/errors/middleware.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/gin-gonic/gin"
88
"github.com/google/uuid"
9+
log "github.com/sirupsen/logrus"
910
)
1011

1112
// ErrorHandlerMiddleware 是一个 Gin 中间件,用于统一处理请求过程中的错误
@@ -53,7 +54,7 @@ func RecoveryMiddleware() gin.HandlerFunc {
5354
}
5455

5556
// 记录错误日志
56-
fmt.Printf("PANIC RECOVERED: %v\n", err)
57+
log.Errorf("PANIC RECOVERED: %v\n", err)
5758

5859
// 返回 500 错误
5960
c.JSON(http.StatusInternalServerError, err)

internal/model/media.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package model
2+
3+
import (
4+
"path/filepath"
5+
)
6+
7+
type Media struct {
8+
Type string `json:"type"` // 媒体类型:image, video, voice, file
9+
Key string `json:"key"` // MD5
10+
Path string `json:"path"`
11+
Name string `json:"name"`
12+
Size int64 `json:"size"`
13+
ModifyTime int64 `json:"modifyTime"`
14+
}
15+
16+
type MediaV3 struct {
17+
Type string `json:"type"`
18+
Key string `json:"key"`
19+
Dir1 string `json:"dir1"`
20+
Dir2 string `json:"dir2"`
21+
Name string `json:"name"`
22+
ModifyTime int64 `json:"modifyTime"`
23+
}
24+
25+
func (m *MediaV3) Wrap() *Media {
26+
27+
var path string
28+
switch m.Type {
29+
case "image":
30+
path = filepath.Join("FileStorage", "MsgAttach", m.Dir1, "Image", m.Dir2, m.Name)
31+
case "video":
32+
path = filepath.Join("FileStorage", "Video", m.Dir2, m.Name)
33+
case "file":
34+
path = filepath.Join("FileStorage", "File", m.Dir2, m.Name)
35+
}
36+
37+
return &Media{
38+
Type: m.Type,
39+
Key: m.Key,
40+
ModifyTime: m.ModifyTime,
41+
Path: path,
42+
Name: m.Name,
43+
}
44+
}

internal/model/media_darwinv3.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package model
2+
3+
import "path/filepath"
4+
5+
// CREATE TABLE HlinkMediaRecord(
6+
// mediaMd5 TEXT,
7+
// mediaSize INTEGER,
8+
// inodeNumber INTEGER,
9+
// modifyTime INTEGER ,
10+
// CONSTRAINT _Md5_Size UNIQUE (mediaMd5,mediaSize)
11+
// )
12+
// CREATE TABLE HlinkMediaDetail(
13+
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
14+
// inodeNumber INTEGER,
15+
// relativePath TEXT,
16+
// fileName TEXT
17+
// )
18+
type MediaDarwinV3 struct {
19+
MediaMd5 string `json:"mediaMd5"`
20+
MediaSize int64 `json:"mediaSize"`
21+
InodeNumber int64 `json:"inodeNumber"`
22+
ModifyTime int64 `json:"modifyTime"`
23+
RelativePath string `json:"relativePath"`
24+
FileName string `json:"fileName"`
25+
}
26+
27+
func (m *MediaDarwinV3) Wrap() *Media {
28+
29+
path := filepath.Join("Message/MessageTemp", m.RelativePath, m.FileName)
30+
name := filepath.Base(path)
31+
32+
return &Media{
33+
Type: "",
34+
Key: m.MediaMd5,
35+
Size: m.MediaSize,
36+
ModifyTime: m.ModifyTime,
37+
Path: path,
38+
Name: name,
39+
}
40+
}

internal/model/media_v4.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package model
2+
3+
import "path/filepath"
4+
5+
type MediaV4 struct {
6+
Type string `json:"type"`
7+
Key string `json:"key"`
8+
Dir1 string `json:"dir1"`
9+
Dir2 string `json:"dir2"`
10+
Name string `json:"name"`
11+
Size int64 `json:"size"`
12+
ModifyTime int64 `json:"modifyTime"`
13+
}
14+
15+
func (m *MediaV4) Wrap() *Media {
16+
17+
var path string
18+
switch m.Type {
19+
case "image":
20+
path = filepath.Join("msg", "attach", m.Dir1, m.Dir2, "Img", m.Name)
21+
case "video":
22+
path = filepath.Join("msg", "video", m.Dir1, m.Name)
23+
case "file":
24+
path = filepath.Join("msg", "file", m.Dir1, m.Name)
25+
}
26+
27+
return &Media{
28+
Type: m.Type,
29+
Key: m.Key,
30+
Path: path,
31+
Name: m.Name,
32+
Size: m.Size,
33+
ModifyTime: m.ModifyTime,
34+
}
35+
}

0 commit comments

Comments
 (0)