Skip to content

Commit a8a2743

Browse files
authored
feat: 支持返回登录二维码与 Docker 部署 (#155)
* feat: 支持返回登录二维码与 Docker 部署 * feat: 完善扫码登录功能 * fix: 修复当存在已经登录的情况,上层还会启动 goroutine的问题,并把 mcp 的返回增加为图片格式
1 parent cc5038d commit a8a2743

File tree

14 files changed

+314
-5
lines changed

14 files changed

+314
-5
lines changed

.dockerignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.git
2+
.idea
3+
.vscode
4+
.claude
5+
.cursor
6+
.github
7+
**/*.log
8+
bin
9+
dist
10+
vendor
11+
Dockerfile
12+
docker-compose.yml
13+
docker
14+
.DS_Store
15+
16+
cookies.json

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*.so
99
*.dylib
1010

11+
.idea
12+
1113
# Test binary, built with `go test -c`
1214
*.test
1315

Dockerfile

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# ---- build stage ----
2+
FROM golang:1.24 AS builder
3+
4+
WORKDIR /src
5+
COPY go.mod go.sum ./
6+
RUN go mod download
7+
8+
COPY . .
9+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /out/app .
10+
11+
# ---- run stage ----
12+
FROM debian:bookworm-slim
13+
14+
WORKDIR /app
15+
16+
# 1. 装 Chromium + 依赖(无头模式运行 rod)
17+
RUN apt-get update && \
18+
apt-get install -y --no-install-recommends \
19+
chromium \
20+
ca-certificates \
21+
fonts-liberation \
22+
fonts-noto-cjk \
23+
libasound2 \
24+
libatk1.0-0 \
25+
libatk-bridge2.0-0 \
26+
libcups2 \
27+
libdrm2 \
28+
libxkbcommon0 \
29+
libxcomposite1 \
30+
libxdamage1 \
31+
libxfixes3 \
32+
libxrandr2 \
33+
libgbm1 \
34+
libpango-1.0-0 \
35+
libnss3 \
36+
libxshmfence1 \
37+
wget \
38+
tzdata \
39+
&& rm -rf /var/lib/apt/lists/*
40+
41+
COPY --from=builder /out/app .
42+
43+
# 2. 设置默认 Chromium 路径(rod 会用)
44+
ENV ROD_BROWSER_BIN=/usr/bin/chromium
45+
46+
EXPOSE 18060
47+
48+
CMD ["./app"]
49+

cookies/cookies.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ func GetCookiesFilePath() string {
5656
return oldPath
5757
}
5858

59+
path := os.Getenv("COOKIES_PATH") // 判断环境变量
60+
if path == "" {
61+
path = "cookies.json" // fallback,本地调试时用当前目录
62+
}
63+
5964
// 文件不存在,使用新路径(当前目录)
60-
return "cookies.json"
65+
return path
6166
}

docker/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Docker 使用说明
2+
<!-- TOC depthFrom:2 -->
3+
4+
- [1. 自己构建镜像](#1-自己构建镜像)
5+
- [2. 手动 Docker Compose](#2-手动 Docker Compose)
6+
7+
## 1. 自己构建镜像
8+
9+
可以使用源码自己构建镜像,如下:
10+
11+
在有项目的Dockerfile的目录运行
12+
13+
`docker build -t xpzouying/xiaohongshu-mcp .`
14+
15+
`xpzouying/xiaohongshu-mcp`为镜像名称和版本,可以自己起个名字
16+
17+
## 2. 手动 Docker Compose
18+
19+
```
20+
# 启动 docker-compose
21+
docker compose up -d
22+
23+
# 停止 docker-compose
24+
docker compose stop
25+
26+
# 查看实时日志
27+
docker logs -f xpzouying/xiaohongshu-mcp
28+
29+
# 进入容器
30+
docker exec -it xpzouying/xiaohongshu-mcp /bin/bash
31+
32+
# 手动更新容器
33+
docker compose pull && docker compose up -d
34+
```

docker/sample/docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
services:
2+
xiaohongshu-mcp:
3+
image: xpzouying/xiaohongshu-mcp
4+
container_name: xiaohongshu-mcp
5+
restart: unless-stopped
6+
tty: true
7+
volumes:
8+
- ./data:/app/data
9+
environment:
10+
- ROD_BROWSER_BIN=/usr/bin/chromium # ← 无头浏览器
11+
- COOKIES_PATH=/app/data/cookies.json # ← 程序读取/写入这个路径
12+
ports:
13+
- "18060:18060"

handlers_api.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ func (s *AppServer) checkLoginStatusHandler(c *gin.Context) {
4848
respondSuccess(c, status, "检查登录状态成功")
4949
}
5050

51+
// getLoginQrcodeHandler 处理 [GET /api/login/qrcode] 请求。
52+
// 用于生成并返回登录二维码(Base64 图片 + 超时时间),供前端展示给用户扫码登录。
53+
func (s *AppServer) getLoginQrcodeHandler(c *gin.Context) {
54+
result, err := s.xiaohongshuService.GetLoginQrcode(c.Request.Context())
55+
if err != nil {
56+
respondError(c, http.StatusInternalServerError, "STATUS_CHECK_FAILED",
57+
"获取登录二维码失败", err.Error())
58+
return
59+
}
60+
61+
respondSuccess(c, result, "获取登录二维码成功")
62+
}
63+
5164
// publishHandler 发布内容
5265
func (s *AppServer) publishHandler(c *gin.Context) {
5366
var req PublishRequest

main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"flag"
5+
"os"
56

67
"github.com/sirupsen/logrus"
78
"github.com/xpzouying/xiaohongshu-mcp/configs"
@@ -16,6 +17,10 @@ func main() {
1617
flag.StringVar(&binPath, "bin", "", "浏览器二进制文件路径")
1718
flag.Parse()
1819

20+
if len(binPath) == 0 {
21+
binPath = os.Getenv("ROD_BROWSER_BIN")
22+
}
23+
1924
configs.InitHeadless(headless)
2025
configs.SetBinPath(binPath)
2126

mcp_handlers.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
87
"github.com/sirupsen/logrus"
8+
"strings"
9+
"time"
910
)
1011

1112
// MCP 工具处理函数
@@ -34,6 +35,46 @@ func (s *AppServer) handleCheckLoginStatus(ctx context.Context) *MCPToolResult {
3435
}
3536
}
3637

38+
// handleGetLoginQrcode 处理获取登录二维码请求。
39+
// 返回二维码图片的 Base64 编码和超时时间,供前端展示扫码登录。
40+
func (s *AppServer) handleGetLoginQrcode(ctx context.Context) *MCPToolResult {
41+
logrus.Info("MCP: 获取登录扫码图片")
42+
43+
result, err := s.xiaohongshuService.GetLoginQrcode(ctx)
44+
if err != nil {
45+
return &MCPToolResult{
46+
Content: []MCPContent{{Type: "text", Text: "获取登录扫码图片失败: " + err.Error()}},
47+
IsError: true,
48+
}
49+
}
50+
51+
if result.IsLoggedIn {
52+
return &MCPToolResult{
53+
Content: []MCPContent{{Type: "text", Text: "你当前已处于登录状态"}},
54+
}
55+
}
56+
57+
now := time.Now()
58+
deadline := func() string {
59+
d, err := time.ParseDuration(result.Timeout)
60+
if err != nil {
61+
return now.Format("2006-01-02 15:04:05")
62+
}
63+
return now.Add(d).Format("2006-01-02 15:04:05")
64+
}()
65+
66+
// 已登录:文本 + 图片
67+
contents := []MCPContent{
68+
{Type: "text", Text: "请用小红书 App 在 " + deadline + " 前扫码登录 👇"},
69+
{
70+
Type: "image",
71+
MimeType: "image/png",
72+
Data: strings.TrimPrefix(result.Img, "data:image/png;base64,"),
73+
},
74+
}
75+
return &MCPToolResult{Content: contents}
76+
}
77+
3778
// handlePublishContent 处理发布内容
3879
func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]interface{}) *MCPToolResult {
3980
logrus.Info("MCP: 发布内容")

routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func setupRoutes(appServer *AppServer) *gin.Engine {
2929
api := router.Group("/api/v1")
3030
{
3131
api.GET("/login/status", appServer.checkLoginStatusHandler)
32+
api.GET("/login/qrcode", appServer.getLoginQrcodeHandler)
3233
api.POST("/publish", appServer.publishHandler)
3334
api.GET("/feeds/list", appServer.listFeedsHandler)
3435
api.GET("/feeds/search", appServer.searchFeedsHandler)

0 commit comments

Comments
 (0)