github-actions[bot] commited on
Commit
c5eab62
·
1 Parent(s): 1352c53

Update from GitHub Actions

Browse files
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用适合Go应用的基础镜像
2
+ FROM golang:alpine AS builder
3
+ ARG TARGETOS
4
+ ARG TARGETARCH
5
+ RUN apk update && apk add --no-cache upx make && rm -rf /var/cache/apk/*
6
+
7
+ # 设置工作目录
8
+ WORKDIR /app
9
+
10
+ # 复制所有文件到容器中
11
+ COPY . .
12
+
13
+ # 下载依赖
14
+ RUN go mod tidy
15
+
16
+ # 构建应用程序
17
+ RUN make build-${TARGETOS}-${TARGETARCH}
18
+
19
+ FROM scratch AS final
20
+ WORKDIR /data
21
+ COPY --from=builder /app/build/monica /data/monica
22
+
23
+ # 开放端口
24
+ EXPOSE 8080
25
+
26
+ # 运行
27
+ CMD ["./monica"]
Makefile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: build
2
+
3
+ build: build-linux-amd64 build-linux-arm64 build-darwin-arm64
4
+
5
+ build-darwin-arm64:
6
+ @rm -rf build || true
7
+ @mkdir -p build || true
8
+ @go mod tidy
9
+ @CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o build/monica .
10
+
11
+ build-linux-amd64:
12
+ @rm -rf build || true
13
+ @mkdir -p build || true
14
+ @go mod tidy
15
+ @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o build/monica .
16
+ @upx -7 build/monica
17
+
18
+ build-linux-arm64:
19
+ @rm -rf build || true
20
+ @mkdir -p build || true
21
+ @go mod tidy
22
+ @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o build/monica .
23
+ @upx -7 build/monica
docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ monica-proxy:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ image: monica-proxy
7
+ container_name: monica-proxy
8
+ restart: unless-stopped
9
+ command: ["./monica"]
10
+ environment:
11
+ - MONICA_COOKIE=${MONICA_COOKIE}
12
+ - BEARER_TOKEN=${BEARER_TOKEN}
13
+
14
+ nginx:
15
+ image: nginx:latest
16
+ container_name: monica-nginx
17
+ ports:
18
+ - "8080:80"
19
+ volumes:
20
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
21
+ depends_on:
22
+ - monica-proxy
go.mod ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module monica-proxy
2
+
3
+ go 1.24
4
+
5
+ toolchain go1.24.0
6
+
7
+ require (
8
+ github.com/bytedance/sonic v1.12.9
9
+ github.com/go-resty/resty/v2 v2.16.5
10
+ github.com/google/uuid v1.6.0
11
+ github.com/labstack/echo/v4 v4.13.3
12
+ github.com/samber/lo v1.49.1
13
+ github.com/sashabaranov/go-openai v1.37.0
14
+ )
15
+
16
+ require github.com/cespare/xxhash/v2 v2.3.0
17
+
18
+ require (
19
+ github.com/bytedance/sonic/loader v0.2.3 // indirect
20
+ github.com/cloudwego/base64x v0.1.5 // indirect
21
+ github.com/joho/godotenv v1.5.1
22
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
23
+ github.com/labstack/gommon v0.4.2 // indirect
24
+ github.com/mattn/go-colorable v0.1.14 // indirect
25
+ github.com/mattn/go-isatty v0.0.20 // indirect
26
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
27
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
28
+ github.com/valyala/fasttemplate v1.2.2 // indirect
29
+ golang.org/x/arch v0.14.0 // indirect
30
+ golang.org/x/crypto v0.35.0 // indirect
31
+ golang.org/x/net v0.35.0 // indirect
32
+ golang.org/x/sys v0.30.0 // indirect
33
+ golang.org/x/text v0.22.0 // indirect
34
+ golang.org/x/time v0.10.0 // indirect
35
+ )
go.sum ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ=
2
+ github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
3
+ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
4
+ github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
5
+ github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
6
+ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
7
+ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
8
+ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
9
+ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
10
+ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
11
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
13
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14
+ github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
15
+ github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
16
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
17
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
18
+ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
19
+ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
20
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
21
+ github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
22
+ github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
23
+ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
24
+ github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
25
+ github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
26
+ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
27
+ github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
28
+ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
29
+ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
30
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
31
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
32
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
33
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
34
+ github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
35
+ github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
36
+ github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY=
37
+ github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
38
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
39
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
40
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
41
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
42
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
43
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
44
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
45
+ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
46
+ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
47
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
48
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
49
+ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
50
+ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
51
+ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
52
+ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
53
+ golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
54
+ golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
55
+ golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
56
+ golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
57
+ golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
58
+ golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
59
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
60
+ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
61
+ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
62
+ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
63
+ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
64
+ golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
65
+ golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
66
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
67
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
68
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
69
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
70
+ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
internal/apiserver/router.go ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package apiserver
2
+
3
+ import (
4
+ "monica-proxy/internal/middleware"
5
+ "monica-proxy/internal/monica"
6
+ "monica-proxy/internal/types"
7
+ "net/http"
8
+
9
+ "github.com/labstack/echo/v4"
10
+ "github.com/sashabaranov/go-openai"
11
+ )
12
+
13
+ // RegisterRoutes 注册 Echo 路由
14
+ func RegisterRoutes(e *echo.Echo) {
15
+ // 添加Bearer Token认证中间件
16
+ e.Use(middleware.BearerAuth())
17
+
18
+ // ChatGPT 风格的请求转发到 /v1/chat/completions
19
+ e.POST("/v1/chat/completions", handleChatCompletion)
20
+ // 获取支持的模型列表
21
+ e.GET("/v1/models", handleListModels)
22
+ }
23
+
24
+ // handleChatCompletion 接收 ChatGPT 形式的对话请求并转发给 Monica
25
+ func handleChatCompletion(c echo.Context) error {
26
+ var req openai.ChatCompletionRequest
27
+ if err := c.Bind(&req); err != nil {
28
+ return c.JSON(http.StatusBadRequest, map[string]interface{}{
29
+ "error": "Invalid request payload",
30
+ })
31
+ }
32
+
33
+ // 检查请求是否包含消息
34
+ if len(req.Messages) == 0 {
35
+ return c.JSON(http.StatusBadRequest, map[string]interface{}{
36
+ "error": "No messages found",
37
+ })
38
+ }
39
+
40
+ // marshalIndent, err := json.MarshalIndent(req, "", " ")
41
+ // if err != nil {
42
+ // return err
43
+ // }
44
+ // log.Printf("Received completion request: \n%s\n", marshalIndent)
45
+ // 将 ChatGPTRequest 转换为 MonicaRequest
46
+ monicaReq, err := types.ChatGPTToMonica(req)
47
+ if err != nil {
48
+ return c.JSON(http.StatusInternalServerError, map[string]interface{}{
49
+ "error": err.Error(),
50
+ })
51
+ }
52
+
53
+ // 调用 Monica 并获取 SSE Stream
54
+ stream, err := monica.SendMonicaRequest(c.Request().Context(), monicaReq)
55
+ if err != nil {
56
+ return c.JSON(http.StatusInternalServerError, map[string]interface{}{
57
+ "error": err.Error(),
58
+ })
59
+ }
60
+ // Resty 不会自动关闭 Body,需要我们自己来处理
61
+ defer stream.RawBody().Close()
62
+
63
+ // 根据 stream 参数决定是否使用流式响应
64
+ if req.Stream {
65
+ // 使用流式响应
66
+ c.Response().Header().Set(echo.HeaderContentType, "text/event-stream")
67
+ c.Response().Header().Set("Cache-Control", "no-cache")
68
+ c.Response().Header().Set("Transfer-Encoding", "chunked")
69
+ c.Response().WriteHeader(http.StatusOK)
70
+
71
+ // 将 Monica 的 SSE 数据逐行读出,再以 SSE 格式返回给调用方
72
+ if err := monica.StreamMonicaSSEToClient(req.Model, c.Response().Writer, stream.RawBody()); err != nil {
73
+ return err
74
+ }
75
+ } else {
76
+ // 使用非流式响应
77
+ c.Response().Header().Set(echo.HeaderContentType, "application/json")
78
+
79
+ // 收集所有的 SSE 数据并转换为完整的响应
80
+ response, err := monica.CollectMonicaSSEToCompletion(req.Model, stream.RawBody())
81
+ if err != nil {
82
+ return c.JSON(http.StatusInternalServerError, map[string]interface{}{
83
+ "error": err.Error(),
84
+ })
85
+ }
86
+
87
+ // 返回完整的响应
88
+ return c.JSON(http.StatusOK, response)
89
+ }
90
+
91
+ return nil
92
+ }
93
+
94
+ // handleListModels 返回支持的模型列表
95
+ func handleListModels(c echo.Context) error {
96
+ models := types.GetSupportedModels()
97
+ return c.JSON(http.StatusOK, models)
98
+ }
internal/config/config.go ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "os"
5
+
6
+ "github.com/joho/godotenv"
7
+ )
8
+
9
+ var MonicaConfig *Config
10
+
11
+ // Config 存储应用配置
12
+ type Config struct {
13
+ MonicaCookie string
14
+ BearerToken string
15
+ }
16
+
17
+ // LoadConfig 从环境变量加载配置
18
+ func LoadConfig() *Config {
19
+ // 尝试加载 .env 文件,但不强制要求文件存在
20
+ _ = godotenv.Load()
21
+
22
+ MonicaConfig = &Config{
23
+ MonicaCookie: os.Getenv("MONICA_COOKIE"),
24
+ BearerToken: os.Getenv("BEARER_TOKEN"),
25
+ }
26
+ return MonicaConfig
27
+ }
internal/middleware/auth.go ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middleware
2
+
3
+ import (
4
+ "log"
5
+ "monica-proxy/internal/config"
6
+ "net/http"
7
+ "strings"
8
+
9
+ "github.com/labstack/echo/v4"
10
+ )
11
+
12
+ // BearerAuth 创建一个Bearer Token认证中间件
13
+ func BearerAuth() echo.MiddlewareFunc {
14
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
15
+ return func(c echo.Context) error {
16
+ // 获取Authorization header
17
+ auth := c.Request().Header.Get("Authorization")
18
+
19
+ // 检查header格式
20
+ if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
21
+ return echo.NewHTTPError(http.StatusUnauthorized, "invalid authorization header")
22
+ }
23
+
24
+ // 提取token
25
+ token := strings.TrimPrefix(auth, "Bearer ")
26
+
27
+ // 验证token
28
+ if token != config.MonicaConfig.BearerToken || token == "" {
29
+ log.Printf("invalid token: %s", token)
30
+ return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
31
+ }
32
+
33
+ return next(c)
34
+ }
35
+ }
36
+ }
internal/monica/client.go ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package monica
2
+
3
+ import (
4
+ "context"
5
+ "github.com/go-resty/resty/v2"
6
+ "log"
7
+ "monica-proxy/internal/config"
8
+ "monica-proxy/internal/types"
9
+ "monica-proxy/internal/utils"
10
+ )
11
+
12
+ // SendMonicaRequest 发起对 Monica AI 的请求(使用 resty)
13
+ func SendMonicaRequest(ctx context.Context, mReq *types.MonicaRequest) (*resty.Response, error) {
14
+ // 发起请求
15
+ resp, err := utils.RestySSEClient.R().
16
+ SetContext(ctx).
17
+ SetHeader("cookie", config.MonicaConfig.MonicaCookie).
18
+ SetBody(mReq).
19
+ Post(types.BotChatURL)
20
+
21
+ if err != nil {
22
+ log.Printf("monica API error: %v", err)
23
+ return nil, err
24
+ }
25
+
26
+ // 如果需要在这里做更多判断,可自行补充
27
+ return resp, nil
28
+ }
internal/monica/sse.go ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package monica
2
+
3
+ import (
4
+ "bufio"
5
+ "bytes"
6
+ "fmt"
7
+ "io"
8
+ "sync"
9
+ "time"
10
+
11
+ "monica-proxy/internal/types"
12
+ "monica-proxy/internal/utils"
13
+ "net/http"
14
+ "strings"
15
+
16
+ "github.com/bytedance/sonic"
17
+ "github.com/sashabaranov/go-openai"
18
+ )
19
+
20
+ const (
21
+ sseObject = "chat.completion.chunk"
22
+ sseFinish = "[DONE]"
23
+ flushInterval = 100 * time.Millisecond // 刷新间隔
24
+ bufferSize = 4096 // 缓冲区大小
25
+
26
+ dataPrefix = "data: "
27
+ dataPrefixLen = len(dataPrefix)
28
+ lineEnd = "\n\n"
29
+ )
30
+
31
+ // SSEData 用于解析 Monica SSE json
32
+ type SSEData struct {
33
+ Text string `json:"text"`
34
+ Finished bool `json:"finished"`
35
+ AgentStatus AgentStatus `json:"agent_status,omitempty"`
36
+ }
37
+
38
+ type AgentStatus struct {
39
+ UID string `json:"uid"`
40
+ Type string `json:"type"`
41
+ Text string `json:"text"`
42
+ Metadata struct {
43
+ Title string `json:"title"`
44
+ ReasoningDetail string `json:"reasoning_detail"`
45
+ } `json:"metadata"`
46
+ }
47
+
48
+ var sseDataPool = sync.Pool{
49
+ New: func() interface{} {
50
+ return &SSEData{}
51
+ },
52
+ }
53
+
54
+ // processMonicaSSE 处理Monica的SSE数据
55
+ type processMonicaSSE struct {
56
+ reader *bufio.Reader
57
+ model string
58
+ buf []byte
59
+ }
60
+
61
+ // handleSSEData 处理单条SSE数据
62
+ type handleSSEData func(*SSEData) error
63
+
64
+ // processSSEStream 处理SSE流
65
+ func (p *processMonicaSSE) processSSEStream(handler handleSSEData) error {
66
+ if p.buf == nil {
67
+ p.buf = make([]byte, 4096)
68
+ }
69
+
70
+ var line []byte
71
+ var err error
72
+ for {
73
+ line, err = p.reader.ReadBytes('\n')
74
+ if err != nil {
75
+ if err == io.EOF {
76
+ return nil
77
+ }
78
+ return fmt.Errorf("read error: %w", err)
79
+ }
80
+
81
+ // Monica SSE 的行前缀一般是 "data: "
82
+ if len(line) < dataPrefixLen || !bytes.HasPrefix(line, []byte(dataPrefix)) {
83
+ continue
84
+ }
85
+
86
+ jsonStr := line[dataPrefixLen : len(line)-1] // 去掉\n
87
+ if len(jsonStr) == 0 {
88
+ continue
89
+ }
90
+
91
+ // 如果是 [DONE] 则结束
92
+ if bytes.Equal(jsonStr, []byte(sseFinish)) {
93
+ return nil
94
+ }
95
+
96
+ // 从对象池获取一个对象
97
+ sseData := sseDataPool.Get().(*SSEData)
98
+
99
+ // 解析 JSON
100
+ if err := sonic.Unmarshal(jsonStr, sseData); err != nil {
101
+ sseData = &SSEData{}
102
+ sseDataPool.Put(sseData)
103
+ return fmt.Errorf("unmarshal error: %w", err)
104
+ }
105
+
106
+ // 调用处理函数
107
+ if err := handler(sseData); err != nil {
108
+ sseData = &SSEData{}
109
+ sseDataPool.Put(sseData)
110
+ return err
111
+ }
112
+
113
+ // 使用完后清理并放回对象池
114
+ sseData = &SSEData{}
115
+ sseDataPool.Put(sseData)
116
+ }
117
+ }
118
+
119
+ // CollectMonicaSSEToCompletion 将 Monica SSE 转换为完整的 ChatCompletion 响应
120
+ func CollectMonicaSSEToCompletion(model string, r io.Reader) (*openai.ChatCompletionResponse, error) {
121
+ var fullContent strings.Builder
122
+ processor := &processMonicaSSE{
123
+ reader: bufio.NewReaderSize(r, bufferSize),
124
+ model: model,
125
+ }
126
+
127
+ // 处理SSE数据
128
+ err := processor.processSSEStream(func(sseData *SSEData) error {
129
+ // 如果是 agent_status,跳过
130
+ if sseData.AgentStatus.Type != "" {
131
+ return nil
132
+ }
133
+ // 累积内容
134
+ fullContent.WriteString(sseData.Text)
135
+ return nil
136
+ })
137
+
138
+ if err != nil {
139
+ return nil, err
140
+ }
141
+
142
+ // 构造完整的响应
143
+ response := &openai.ChatCompletionResponse{
144
+ ID: fmt.Sprintf("chatcmpl-%s", utils.RandStringUsingMathRand(29)),
145
+ Object: "chat.completion",
146
+ Created: time.Now().Unix(),
147
+ Model: model,
148
+ Choices: []openai.ChatCompletionChoice{
149
+ {
150
+ Index: 0,
151
+ Message: openai.ChatCompletionMessage{
152
+ Role: "assistant",
153
+ Content: fullContent.String(),
154
+ },
155
+ FinishReason: "stop",
156
+ },
157
+ },
158
+ Usage: openai.Usage{
159
+ // Monica API 不提供 token 使用信息,这里暂时填 0
160
+ PromptTokens: 0,
161
+ CompletionTokens: 0,
162
+ TotalTokens: 0,
163
+ },
164
+ }
165
+
166
+ return response, nil
167
+ }
168
+
169
+ // StreamMonicaSSEToClient 将 Monica SSE 转成前端可用的流
170
+ func StreamMonicaSSEToClient(model string, w io.Writer, r io.Reader) error {
171
+ writer := bufio.NewWriterSize(w, bufferSize)
172
+ defer writer.Flush()
173
+
174
+ chatId := utils.RandStringUsingMathRand(29)
175
+ now := time.Now().Unix()
176
+ fingerprint := utils.RandStringUsingMathRand(10)
177
+
178
+ // 创建一个定时刷新的 ticker
179
+ ticker := time.NewTicker(flushInterval)
180
+ defer ticker.Stop()
181
+
182
+ // 创建一个 done channel 用于清理
183
+ done := make(chan struct{})
184
+ defer close(done)
185
+
186
+ // 启动一个 goroutine 定期刷新缓冲区
187
+ go func() {
188
+ for {
189
+ select {
190
+ case <-ticker.C:
191
+ if f, ok := w.(http.Flusher); ok {
192
+ writer.Flush()
193
+ f.Flush()
194
+ }
195
+ case <-done:
196
+ return
197
+ }
198
+ }
199
+ }()
200
+
201
+ processor := &processMonicaSSE{
202
+ reader: bufio.NewReaderSize(r, bufferSize),
203
+ model: model,
204
+ }
205
+
206
+ var thinkFlag bool
207
+ return processor.processSSEStream(func(sseData *SSEData) error {
208
+ var sseMsg types.ChatCompletionStreamResponse
209
+ switch {
210
+ case sseData.Finished:
211
+ sseMsg = types.ChatCompletionStreamResponse{
212
+ ID: "chatcmpl-" + chatId,
213
+ Object: sseObject,
214
+ Created: now,
215
+ Model: model,
216
+ Choices: []types.ChatCompletionStreamChoice{
217
+ {
218
+ Index: 0,
219
+ Delta: openai.ChatCompletionStreamChoiceDelta{
220
+ Role: openai.ChatMessageRoleAssistant,
221
+ },
222
+ FinishReason: openai.FinishReasonStop,
223
+ },
224
+ },
225
+ }
226
+ case sseData.AgentStatus.Type == "thinking":
227
+ thinkFlag = true
228
+ sseMsg = types.ChatCompletionStreamResponse{
229
+ ID: "chatcmpl-" + chatId,
230
+ Object: sseObject,
231
+ SystemFingerprint: fingerprint,
232
+ Created: now,
233
+ Model: model,
234
+ Choices: []types.ChatCompletionStreamChoice{
235
+ {
236
+ Index: 0,
237
+ Delta: openai.ChatCompletionStreamChoiceDelta{
238
+ Role: openai.ChatMessageRoleAssistant,
239
+ Content: `<think>`,
240
+ },
241
+ FinishReason: openai.FinishReasonNull,
242
+ },
243
+ },
244
+ }
245
+ case sseData.AgentStatus.Type == "thinking_detail_stream":
246
+ sseMsg = types.ChatCompletionStreamResponse{
247
+ ID: "chatcmpl-" + chatId,
248
+ Object: sseObject,
249
+ SystemFingerprint: fingerprint,
250
+ Created: now,
251
+ Model: model,
252
+ Choices: []types.ChatCompletionStreamChoice{
253
+ {
254
+ Index: 0,
255
+ Delta: openai.ChatCompletionStreamChoiceDelta{
256
+ Role: openai.ChatMessageRoleAssistant,
257
+ Content: sseData.AgentStatus.Metadata.ReasoningDetail,
258
+ },
259
+ FinishReason: openai.FinishReasonNull,
260
+ },
261
+ },
262
+ }
263
+ default:
264
+ if thinkFlag {
265
+ sseData.Text = "</think>" + sseData.Text
266
+ thinkFlag = false
267
+ }
268
+ sseMsg = types.ChatCompletionStreamResponse{
269
+ ID: "chatcmpl-" + chatId,
270
+ Object: sseObject,
271
+ SystemFingerprint: fingerprint,
272
+ Created: now,
273
+ Model: model,
274
+ Choices: []types.ChatCompletionStreamChoice{
275
+ {
276
+ Index: 0,
277
+ Delta: openai.ChatCompletionStreamChoiceDelta{
278
+ Role: openai.ChatMessageRoleAssistant,
279
+ Content: sseData.Text,
280
+ },
281
+ FinishReason: openai.FinishReasonNull,
282
+ },
283
+ },
284
+ }
285
+ }
286
+
287
+ var sb strings.Builder
288
+ sb.WriteString("data: ")
289
+ sendLine, _ := sonic.MarshalString(sseMsg)
290
+ sb.WriteString(sendLine)
291
+ sb.WriteString("\n\n")
292
+
293
+ // 写入缓冲区
294
+ if _, err := writer.WriteString(sb.String()); err != nil {
295
+ return fmt.Errorf("write error: %w", err)
296
+ }
297
+
298
+ // 如果发现 finished=true,就可以结束
299
+ if sseData.Finished {
300
+ writer.WriteString(dataPrefix)
301
+ writer.WriteString(sseFinish)
302
+ writer.WriteString(lineEnd)
303
+ writer.Flush()
304
+ if f, ok := w.(http.Flusher); ok {
305
+ f.Flush()
306
+ }
307
+ return nil
308
+ }
309
+
310
+ sseData.AgentStatus.Type = ""
311
+ sseData.Finished = false
312
+ return nil
313
+ })
314
+ }
internal/types/image.go ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package types
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "monica-proxy/internal/config"
7
+ "monica-proxy/internal/utils"
8
+ "net/http"
9
+ "strings"
10
+ "sync"
11
+ "time"
12
+
13
+ "github.com/cespare/xxhash/v2"
14
+ "github.com/google/uuid"
15
+ )
16
+
17
+ const MaxFileSize = 10 * 1024 * 1024 // 10MB
18
+
19
+ var imageCache sync.Map
20
+
21
+ // sampleAndHash 对base64字符串进行采样并计算xxHash
22
+ func sampleAndHash(data string) string {
23
+ // 如果数据长度小于1024,直接计算整个字符串的哈希
24
+ if len(data) <= 1024 {
25
+ return fmt.Sprintf("%x", xxhash.Sum64String(data))
26
+ }
27
+
28
+ // 采样策略:
29
+ // 1. 取前256字节
30
+ // 2. 取中间256字节
31
+ // 3. 取最后256字节
32
+ var samples []string
33
+ samples = append(samples, data[:256])
34
+ mid := len(data) / 2
35
+ samples = append(samples, data[mid-128:mid+128])
36
+ samples = append(samples, data[len(data)-256:])
37
+
38
+ // 将采样数据拼接后计算哈希
39
+ return fmt.Sprintf("%x", xxhash.Sum64String(strings.Join(samples, "")))
40
+ }
41
+
42
+ // UploadBase64Image 上传base64编码的图片到Monica
43
+ func UploadBase64Image(ctx context.Context, base64Data string) (*FileInfo, error) {
44
+ // 1. 生成缓存key
45
+ cacheKey := sampleAndHash(base64Data)
46
+
47
+ // 2. 检查缓存
48
+ if value, exists := imageCache.Load(cacheKey); exists {
49
+ return value.(*FileInfo), nil
50
+ }
51
+
52
+ // 3. 解析base64数据
53
+ // 移除 "data:image/png;base64," 这样的前缀
54
+ parts := strings.Split(base64Data, ",")
55
+ if len(parts) != 2 {
56
+ return nil, fmt.Errorf("invalid base64 image format")
57
+ }
58
+
59
+ // 获取图片类型
60
+ mimeType := strings.TrimSuffix(strings.TrimPrefix(parts[0], "data:"), ";base64")
61
+ if !strings.HasPrefix(mimeType, "image/") {
62
+ return nil, fmt.Errorf("invalid image mime type: %s", mimeType)
63
+ }
64
+
65
+ // 解码base64数据
66
+ imageData, err := utils.Base64Decode(parts[1])
67
+ if err != nil {
68
+ return nil, fmt.Errorf("decode base64 failed: %v", err)
69
+ }
70
+
71
+ // 4. 验证图片格式和大小
72
+ fileInfo, err := validateImageBytes(imageData, mimeType)
73
+ if err != nil {
74
+ return nil, fmt.Errorf("validate image failed: %v", err)
75
+ }
76
+ // log.Printf("file info: %+v", fileInfo)
77
+
78
+ // 5. 获取预签名URL
79
+ preSignReq := &PreSignRequest{
80
+ FilenameList: []string{fileInfo.FileName},
81
+ Module: ImageModule,
82
+ Location: ImageLocation,
83
+ ObjID: uuid.New().String(),
84
+ }
85
+
86
+ var preSignResp PreSignResponse
87
+ _, err = utils.RestyDefaultClient.R().
88
+ SetContext(ctx).
89
+ SetHeader("cookie", config.MonicaConfig.MonicaCookie).
90
+ SetBody(preSignReq).
91
+ SetResult(&preSignResp).
92
+ Post(PreSignURL)
93
+
94
+ if err != nil {
95
+ return nil, fmt.Errorf("get pre-sign url failed: %v", err)
96
+ }
97
+
98
+ if len(preSignResp.Data.PreSignURLList) == 0 || len(preSignResp.Data.ObjectURLList) == 0 {
99
+ return nil, fmt.Errorf("no pre-sign url or object url returned")
100
+ }
101
+ // log.Printf("preSign info: %+v", preSignResp)
102
+
103
+ // 6. 上传图片数据
104
+ _, err = utils.RestyDefaultClient.R().
105
+ SetContext(ctx).
106
+ SetHeader("Content-Type", fileInfo.FileType).
107
+ SetBody(imageData).
108
+ Put(preSignResp.Data.PreSignURLList[0])
109
+
110
+ if err != nil {
111
+ return nil, fmt.Errorf("upload file failed: %v", err)
112
+ }
113
+
114
+ // 7. 创建文件对象
115
+ fileInfo.ObjectURL = preSignResp.Data.ObjectURLList[0]
116
+ uploadReq := &FileUploadRequest{
117
+ Data: []FileInfo{*fileInfo},
118
+ }
119
+
120
+ var uploadResp FileUploadResponse
121
+ _, err = utils.RestyDefaultClient.R().
122
+ SetContext(ctx).
123
+ SetHeader("cookie", config.MonicaConfig.MonicaCookie).
124
+ SetBody(uploadReq).
125
+ SetResult(&uploadResp).
126
+ Post(FileUploadURL)
127
+
128
+ if err != nil {
129
+ return nil, fmt.Errorf("create file object failed: %v", err)
130
+ }
131
+ // log.Printf("uploadResp: %+v", uploadResp)
132
+ if len(uploadResp.Data.Items) > 0 {
133
+ fileInfo.FileName = uploadResp.Data.Items[0].FileName
134
+ fileInfo.FileType = uploadResp.Data.Items[0].FileType
135
+ fileInfo.FileSize = uploadResp.Data.Items[0].FileSize
136
+ fileInfo.FileUID = uploadResp.Data.Items[0].FileUID
137
+ fileInfo.FileExt = uploadResp.Data.Items[0].FileType
138
+ fileInfo.FileTokens = uploadResp.Data.Items[0].FileTokens
139
+ fileInfo.FileChunks = uploadResp.Data.Items[0].FileChunks
140
+ }
141
+
142
+ fileInfo.UseFullText = true
143
+ fileInfo.FileURL = preSignResp.Data.CDNURLList[0]
144
+
145
+ // 8. 获取文件llm读取结果知道有返回
146
+ var batchResp FileBatchGetResponse
147
+ reqMap := make(map[string][]string)
148
+ reqMap["file_uids"] = []string{fileInfo.FileUID}
149
+ var retryCount = 1
150
+ for {
151
+ if retryCount > 5 {
152
+ return nil, fmt.Errorf("retry limit exceeded")
153
+ }
154
+ _, err = utils.RestyDefaultClient.R().
155
+ SetContext(ctx).
156
+ SetHeader("cookie", config.MonicaConfig.MonicaCookie).
157
+ SetBody(reqMap).
158
+ SetResult(&batchResp).
159
+ Post(FileGetURL)
160
+ if err != nil {
161
+ return nil, fmt.Errorf("batch get file failed: %v", err)
162
+ }
163
+ if len(batchResp.Data.Items) > 0 && batchResp.Data.Items[0].FileChunks > 0 {
164
+ break
165
+ } else {
166
+ retryCount++
167
+ }
168
+ time.Sleep(1 * time.Second)
169
+ }
170
+ fileInfo.FileChunks = batchResp.Data.Items[0].FileChunks
171
+ fileInfo.FileTokens = batchResp.Data.Items[0].FileTokens
172
+ fileInfo.URL = ""
173
+ fileInfo.ObjectURL = ""
174
+
175
+ // 9. 保存到缓存
176
+ imageCache.Store(cacheKey, fileInfo)
177
+
178
+ return fileInfo, nil
179
+ }
180
+
181
+ // validateImageBytes 验证图片字节数据的格式和大小
182
+ func validateImageBytes(imageData []byte, mimeType string) (*FileInfo, error) {
183
+ if len(imageData) > MaxFileSize {
184
+ return nil, fmt.Errorf("file size exceeds limit: %d > %d", len(imageData), MaxFileSize)
185
+ }
186
+
187
+ contentType := http.DetectContentType(imageData)
188
+ if !SupportedImageTypes[contentType] {
189
+ return nil, fmt.Errorf("unsupported image type: %s", contentType)
190
+ }
191
+
192
+ // 根据MIME类型生成文件扩展名
193
+ ext := ".png"
194
+ switch mimeType {
195
+ case "image/jpeg":
196
+ ext = ".jpg"
197
+ case "image/gif":
198
+ ext = ".gif"
199
+ case "image/webp":
200
+ ext = ".webp"
201
+ }
202
+
203
+ fileName := fmt.Sprintf("%s%s", uuid.New().String(), ext)
204
+
205
+ return &FileInfo{
206
+ FileName: fileName,
207
+ FileSize: int64(len(imageData)),
208
+ FileType: contentType,
209
+ }, nil
210
+ }
internal/types/monica.go ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package types
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "log"
7
+ "strings"
8
+
9
+ lop "github.com/samber/lo/parallel"
10
+
11
+ "github.com/google/uuid"
12
+ "github.com/sashabaranov/go-openai"
13
+ )
14
+
15
+ const (
16
+ BotChatURL = "https://api.monica.im/api/custom_bot/chat"
17
+ PreSignURL = "https://api.monica.im/api/file_object/pre_sign_list_by_module"
18
+ FileUploadURL = "https://api.monica.im/api/files/batch_create_llm_file"
19
+ FileGetURL = "https://api.monica.im/api/files/batch_get_file"
20
+ )
21
+
22
+ // 图片相关常量
23
+ const (
24
+ MaxImageSize = 10 * 1024 * 1024 // 10MB
25
+ ImageModule = "chat_bot"
26
+ ImageLocation = "files"
27
+ )
28
+
29
+ // 支持的图片格式
30
+ var SupportedImageTypes = map[string]bool{
31
+ "image/jpeg": true,
32
+ "image/png": true,
33
+ "image/gif": true,
34
+ "image/webp": true,
35
+ }
36
+
37
+ type ChatGPTRequest struct {
38
+ Model string `json:"model"` // gpt-3.5-turbo, gpt-4, ...
39
+ Messages []ChatMessage `json:"messages"` // 对话数组
40
+ Stream bool `json:"stream"` // 是否流式返回
41
+ }
42
+
43
+ type ChatMessage struct {
44
+ Role string `json:"role"` // "system", "user", "assistant"
45
+ Content interface{} `json:"content"` // 可以是字符串或MessageContent数组
46
+ }
47
+
48
+ // MessageContent 消息内容
49
+ type MessageContent struct {
50
+ Type string `json:"type"` // "text" 或 "image_url"
51
+ Text string `json:"text,omitempty"` // 文本内容
52
+ ImageURL string `json:"image_url,omitempty"` // 图片URL
53
+ }
54
+
55
+ // MonicaRequest 为 Monica 自定义 AI 的请求格式
56
+ type MonicaRequest struct {
57
+ TaskUID string `json:"task_uid"`
58
+ BotUID string `json:"bot_uid"`
59
+ Data DataField `json:"data"`
60
+ Language string `json:"language"`
61
+ TaskType string `json:"task_type"`
62
+ ToolData ToolData `json:"tool_data"`
63
+ }
64
+
65
+ // DataField 在 Monica 的 body 中
66
+ type DataField struct {
67
+ ConversationID string `json:"conversation_id"`
68
+ PreParentItemID string `json:"pre_parent_item_id"`
69
+ Items []Item `json:"items"`
70
+ TriggerBy string `json:"trigger_by"`
71
+ UseModel string `json:"use_model,omitempty"`
72
+ IsIncognito bool `json:"is_incognito"`
73
+ UseNewMemory bool `json:"use_new_memory"`
74
+ }
75
+
76
+ type Item struct {
77
+ ConversationID string `json:"conversation_id"`
78
+ ParentItemID string `json:"parent_item_id,omitempty"`
79
+ ItemID string `json:"item_id"`
80
+ ItemType string `json:"item_type"`
81
+ Data ItemContent `json:"data"`
82
+ }
83
+
84
+ type ItemContent struct {
85
+ Type string `json:"type"`
86
+ Content string `json:"content"`
87
+ MaxToken int `json:"max_token,omitempty"`
88
+ IsIncognito bool `json:"is_incognito,omitempty"` // 是否无痕模式
89
+ FromTaskType string `json:"from_task_type,omitempty"`
90
+ ManualWebSearchEnabled bool `json:"manual_web_search_enabled,omitempty"` // 网页搜索
91
+ UseModel string `json:"use_model,omitempty"`
92
+ FileInfos []FileInfo `json:"file_infos,omitempty"`
93
+ }
94
+
95
+ // ToolData 这里演示放空
96
+ type ToolData struct {
97
+ SysSkillList []string `json:"sys_skill_list"`
98
+ }
99
+
100
+ // PreSignRequest 预签名请求
101
+ type PreSignRequest struct {
102
+ FilenameList []string `json:"filename_list"`
103
+ Module string `json:"module"`
104
+ Location string `json:"location"`
105
+ ObjID string `json:"obj_id"`
106
+ }
107
+
108
+ // PreSignResponse 预签名响应
109
+ type PreSignResponse struct {
110
+ Code int `json:"code"`
111
+ Msg string `json:"msg"`
112
+ Data struct {
113
+ PreSignURLList []string `json:"pre_sign_url_list"`
114
+ ObjectURLList []string `json:"object_url_list"`
115
+ CDNURLList []string `json:"cdn_url_list"`
116
+ } `json:"data"`
117
+ }
118
+
119
+ // FileInfo 文件信息
120
+ type FileInfo struct {
121
+ URL string `json:"url,omitempty"`
122
+ FileURL string `json:"file_url"`
123
+ FileUID string `json:"file_uid"`
124
+ Parse bool `json:"parse"`
125
+ FileName string `json:"file_name"`
126
+ FileSize int64 `json:"file_size"`
127
+ FileType string `json:"file_type"`
128
+ FileExt string `json:"file_ext"`
129
+ FileTokens int64 `json:"file_tokens"`
130
+ FileChunks int64 `json:"file_chunks"`
131
+ ObjectURL string `json:"object_url,omitempty"`
132
+ //Embedding bool `json:"embedding"`
133
+ FileMetaInfo map[string]interface{} `json:"file_meta_info,omitempty"`
134
+ UseFullText bool `json:"use_full_text"`
135
+ }
136
+
137
+ // FileUploadRequest 文件上传请求
138
+ type FileUploadRequest struct {
139
+ Data []FileInfo `json:"data"`
140
+ }
141
+
142
+ // FileUploadResponse 文件上传响应
143
+ type FileUploadResponse struct {
144
+ Code int `json:"code"`
145
+ Msg string `json:"msg"`
146
+ Data struct {
147
+ Items []struct {
148
+ FileName string `json:"file_name"`
149
+ FileType string `json:"file_type"`
150
+ FileSize int64 `json:"file_size"`
151
+ FileUID string `json:"file_uid"`
152
+ FileTokens int64 `json:"file_tokens"`
153
+ FileChunks int64 `json:"file_chunks"`
154
+ // 其他字段暂时不需要
155
+ } `json:"items"`
156
+ } `json:"data"`
157
+ }
158
+
159
+ // FileBatchGetResponse 获取文件llm处理是否完成
160
+ type FileBatchGetResponse struct {
161
+ Data struct {
162
+ Items []struct {
163
+ FileName string `json:"file_name"`
164
+ FileType string `json:"file_type"`
165
+ FileSize int `json:"file_size"`
166
+ ObjectUrl string `json:"object_url"`
167
+ Url string `json:"url"`
168
+ FileMetaInfo struct {
169
+ } `json:"file_meta_info"`
170
+ DriveFileUid string `json:"drive_file_uid"`
171
+ FileUid string `json:"file_uid"`
172
+ IndexState int `json:"index_state"`
173
+ IndexDesc string `json:"index_desc"`
174
+ ErrorMessage string `json:"error_message"`
175
+ FileTokens int64 `json:"file_tokens"`
176
+ FileChunks int64 `json:"file_chunks"`
177
+ IndexProgress int `json:"index_progress"`
178
+ } `json:"items"`
179
+ } `json:"data"`
180
+ }
181
+
182
+ // OpenAIModel represents a model in the OpenAI API format
183
+ type OpenAIModel struct {
184
+ ID string `json:"id"`
185
+ Object string `json:"object"`
186
+ OwnedBy string `json:"owned_by"`
187
+ }
188
+
189
+ // OpenAIModelList represents the response format for the /v1/models endpoint
190
+ type OpenAIModelList struct {
191
+ Object string `json:"object"`
192
+ Data []OpenAIModel `json:"data"`
193
+ }
194
+
195
+ // GetSupportedModels returns all supported models in OpenAI format
196
+ func GetSupportedModels() OpenAIModelList {
197
+ models := []OpenAIModel{
198
+ {ID: "gpt-4o-mini", Object: "model", OwnedBy: "monica"},
199
+ {ID: "gpt-4o", Object: "model", OwnedBy: "monica"},
200
+ {ID: "claude-3-7-sonnet", Object: "model", OwnedBy: "monica"},
201
+ {ID: "claude-3-5-sonnet", Object: "model", OwnedBy: "monica"},
202
+ {ID: "claude-3-5-haiku", Object: "model", OwnedBy: "monica"},
203
+ {ID: "gemini-2.0-pro", Object: "model", OwnedBy: "monica"},
204
+ {ID: "gemini-2.0-flash", Object: "model", OwnedBy: "monica"},
205
+ {ID: "gemini-1.5-pro", Object: "model", OwnedBy: "monica"},
206
+ {ID: "o3-mini", Object: "model", OwnedBy: "monica"},
207
+ {ID: "o1-preview", Object: "model", OwnedBy: "monica"},
208
+ {ID: "deepseek-reasoner", Object: "model", OwnedBy: "monica"},
209
+ {ID: "deepseek-chat", Object: "model", OwnedBy: "monica"},
210
+ {ID: "deepclaude", Object: "model", OwnedBy: "monica"},
211
+ {ID: "sonar", Object: "model", OwnedBy: "monica"},
212
+ }
213
+
214
+ return OpenAIModelList{
215
+ Object: "list",
216
+ Data: models,
217
+ }
218
+ }
219
+
220
+ // ChatGPTToMonica 将 ChatGPTRequest 转换为 MonicaRequest
221
+ func ChatGPTToMonica(chatReq openai.ChatCompletionRequest) (*MonicaRequest, error) {
222
+ if len(chatReq.Messages) == 0 {
223
+ return nil, fmt.Errorf("empty messages")
224
+ }
225
+
226
+ // 生成会话ID
227
+ conversationID := fmt.Sprintf("conv:%s", uuid.New().String())
228
+
229
+ // 转换消息
230
+
231
+ // 设置默认欢迎消息头,不加上就有几率去掉问题最后的十几个token,不清楚是不是bug
232
+ defaultItem := Item{
233
+ ItemID: fmt.Sprintf("msg:%s", uuid.New().String()),
234
+ ConversationID: conversationID,
235
+ ItemType: "reply",
236
+ Data: ItemContent{Type: "text", Content: "__RENDER_BOT_WELCOME_MSG__"},
237
+ }
238
+ var items = make([]Item, 1, len(chatReq.Messages))
239
+ items[0] = defaultItem
240
+ preItemID := defaultItem.ItemID
241
+
242
+ for _, msg := range chatReq.Messages {
243
+ if msg.Role == "system" {
244
+ // monica不支持设置prompt,所以直接跳过
245
+ continue
246
+ }
247
+ var msgContext string
248
+ var imgUrl []*openai.ChatMessageImageURL
249
+ if len(msg.MultiContent) > 0 { // 说明应该是多内容,可能是图片内容
250
+ for _, content := range msg.MultiContent {
251
+ switch content.Type {
252
+ case "text":
253
+ msgContext = content.Text
254
+ case "image_url":
255
+ imgUrl = append(imgUrl, content.ImageURL)
256
+ }
257
+ }
258
+ }
259
+ itemID := fmt.Sprintf("msg:%s", uuid.New().String())
260
+ itemType := "question"
261
+ if msg.Role == "assistant" {
262
+ itemType = "reply"
263
+ }
264
+
265
+ var content ItemContent
266
+ if len(imgUrl) > 0 {
267
+ ctx := context.Background()
268
+ fileIfoList := lop.Map(imgUrl, func(item *openai.ChatMessageImageURL, _ int) FileInfo {
269
+ f, err := UploadBase64Image(ctx, item.URL)
270
+ if err != nil {
271
+ log.Println(err)
272
+ return FileInfo{}
273
+ }
274
+ return *f
275
+ })
276
+
277
+ content = ItemContent{
278
+ Type: "file_with_text",
279
+ Content: msgContext,
280
+ FileInfos: fileIfoList,
281
+ IsIncognito: true,
282
+ }
283
+ } else {
284
+ content = ItemContent{
285
+ Type: "text",
286
+ Content: msg.Content,
287
+ IsIncognito: true,
288
+ }
289
+ }
290
+
291
+ item := Item{
292
+ ConversationID: conversationID,
293
+ ItemID: itemID,
294
+ ParentItemID: preItemID,
295
+ ItemType: itemType,
296
+ Data: content,
297
+ }
298
+ items = append(items, item)
299
+ preItemID = itemID
300
+ }
301
+
302
+ // 构建请求
303
+ mReq := &MonicaRequest{
304
+ TaskUID: fmt.Sprintf("task:%s", uuid.New().String()),
305
+ BotUID: modelToBot(chatReq.Model),
306
+ Data: DataField{
307
+ ConversationID: conversationID,
308
+ Items: items,
309
+ PreParentItemID: preItemID,
310
+ TriggerBy: "auto",
311
+ IsIncognito: true,
312
+ UseModel: "", //TODO 好像写啥都没影响
313
+ UseNewMemory: false,
314
+ },
315
+ Language: "auto",
316
+ TaskType: "chat",
317
+ }
318
+
319
+ // indent, err := json.MarshalIndent(mReq, "", " ")
320
+ // if err != nil {
321
+ // return nil, err
322
+ // }
323
+ // log.Printf("send: \n%s\n", indent)
324
+
325
+ return mReq, nil
326
+ }
327
+
328
+ func modelToBot(model string) string {
329
+ switch {
330
+ case strings.HasPrefix(model, "gpt-4o-mini"):
331
+ return "gpt_4_o_mini_chat"
332
+ case strings.HasPrefix(model, "gpt-4o"):
333
+ return "gpt_4_o_chat"
334
+ case strings.HasPrefix(model, "claude-3-7-sonnet"):
335
+ return "claude_3_7_sonnet"
336
+ case strings.HasPrefix(model, "claude-3-5-sonnet"):
337
+ return "claude_3.5_sonnet"
338
+ case strings.HasPrefix(model, "claude-3-5-haiku"):
339
+ return "claude_3.5_haiku"
340
+ case strings.HasPrefix(model, "gemini-2.0-pro"):
341
+ return "gemini_2_0_pro"
342
+ case strings.HasPrefix(model, "gemini-2.0-flash"):
343
+ return "gemini_2_0"
344
+ case strings.HasPrefix(model, "gemini-1"):
345
+ return "gemini_1_5"
346
+ case strings.HasPrefix(model, "o1-preview"):
347
+ return "openai_o_1"
348
+ case strings.HasPrefix(model, "o3-mini"):
349
+ return "openai_o_3_mini"
350
+ case model == "deepseek-reasoner":
351
+ return "deepseek_reasoner"
352
+ case model == "deepseek-chat":
353
+ return "deepseek_chat"
354
+ case model == "deepclaude":
355
+ return "deepclaude"
356
+ case model == "sonar":
357
+ return "sonar"
358
+ default:
359
+ return model
360
+ }
361
+ }
internal/types/openai.go ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package types
2
+
3
+ import "github.com/sashabaranov/go-openai"
4
+
5
+ type ChatCompletionStreamResponse struct {
6
+ ID string `json:"id"`
7
+ Object string `json:"object"`
8
+ Created int64 `json:"created"`
9
+ Model string `json:"model"`
10
+ Choices []ChatCompletionStreamChoice `json:"choices"`
11
+ SystemFingerprint string `json:"system_fingerprint"`
12
+ PromptAnnotations []openai.PromptAnnotation `json:"prompt_annotations,omitempty"`
13
+ PromptFilterResults []openai.PromptFilterResult `json:"prompt_filter_results,omitempty"`
14
+ Usage *openai.Usage `json:"usage,omitempty"`
15
+ }
16
+
17
+ type ChatCompletionStreamChoice struct {
18
+ Index int `json:"index"`
19
+ Delta openai.ChatCompletionStreamChoiceDelta `json:"delta"`
20
+ Logprobs *openai.ChatCompletionStreamChoiceLogprobs `json:"logprobs,omitempty"`
21
+ FinishReason openai.FinishReason `json:"finish_reason"`
22
+ }
internal/utils/base64.go ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package utils
2
+
3
+ import (
4
+ "encoding/base64"
5
+ )
6
+
7
+ // Base64Decode 解码base64字符串为字节数组
8
+ func Base64Decode(data string) ([]byte, error) {
9
+ return base64.StdEncoding.DecodeString(data)
10
+ }
internal/utils/req_client.go ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package utils
2
+
3
+ import (
4
+ "crypto/tls"
5
+ "fmt"
6
+ "github.com/go-resty/resty/v2"
7
+ "time"
8
+ )
9
+
10
+ var (
11
+ RestySSEClient = resty.New().
12
+ SetTimeout(3 * time.Minute).
13
+ SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).
14
+ SetDoNotParseResponse(true). // 告诉 Resty,不要自动读取/解析 Body,让我们自己来处理流
15
+ SetHeaders(map[string]string{
16
+ "Content-Type": "application/json",
17
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
18
+ "x-client-locale": "zh_CN", // 可以不传,但默认会返回英文回答
19
+ }).
20
+ OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
21
+ // 如果不是 200,尝试把 body 打印出来
22
+ if resp.StatusCode() != 200 {
23
+ return fmt.Errorf("monica API error: status %d, body: %s",
24
+ resp.StatusCode(), resp.String())
25
+ }
26
+ return nil
27
+ })
28
+
29
+ RestyDefaultClient = resty.New().
30
+ SetTimeout(time.Second * 30).
31
+ SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).
32
+ SetHeaders(map[string]string{
33
+ "Content-Type": "application/json",
34
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
35
+ }).
36
+ OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
37
+ // 如果不是 200,尝试把 body 打印出来
38
+ if resp.StatusCode() != 200 {
39
+ return fmt.Errorf("monica API error: status %d, body: %s",
40
+ resp.StatusCode(), resp.String())
41
+ }
42
+ return nil
43
+ })
44
+ )
internal/utils/string.go ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package utils
2
+
3
+ import (
4
+ "math/rand"
5
+ "time"
6
+ )
7
+
8
+ var randSource = rand.New(rand.NewSource(time.Now().UnixNano()))
9
+
10
+ // RandStringUsingMathRand 生成指定长度的随机字符串
11
+ func RandStringUsingMathRand(n int) string {
12
+ var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
13
+
14
+ // 创建一个长度为 n 的切片,用来存放随机字符
15
+ result := make([]rune, n)
16
+ for i := 0; i < n; i++ {
17
+ result[i] = letters[randSource.Intn(len(letters))]
18
+ }
19
+
20
+ return string(result)
21
+ }
main.go ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "errors"
5
+ "log"
6
+ "monica-proxy/internal/apiserver"
7
+ "monica-proxy/internal/config"
8
+ "net/http"
9
+
10
+ "github.com/labstack/echo/v4/middleware"
11
+
12
+ "github.com/labstack/echo/v4"
13
+ )
14
+
15
+ func main() {
16
+ // 加载配置
17
+ cfg := config.LoadConfig()
18
+ if cfg.MonicaCookie == "" {
19
+ log.Fatal("MONICA_COOKIE environment variable is required")
20
+ }
21
+
22
+ e := echo.New()
23
+ e.Use(middleware.Logger())
24
+ e.Use(middleware.Recover())
25
+ // 注册路由
26
+ apiserver.RegisterRoutes(e)
27
+ // 启动服务
28
+ if err := e.Start("0.0.0.0:8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
29
+ log.Fatalf("start server error: %v", err)
30
+ }
31
+ }
nginx.conf ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ events {
2
+ worker_connections 1024;
3
+ }
4
+
5
+ http {
6
+ upstream monica-proxy {
7
+ server monica-proxy:8080;
8
+ }
9
+
10
+ server {
11
+ listen 80;
12
+
13
+ location / {
14
+ # 必须使用 HTTP/1.1,才能支持 chunked 传输
15
+ proxy_http_version 1.1;
16
+ # 去掉 Connection: close,避免长连接被关闭
17
+ proxy_set_header Connection '';
18
+
19
+ # 指定后端地址
20
+ proxy_pass http://monica-proxy;
21
+
22
+ # 关闭 Nginx 的各种缓存与缓冲
23
+ proxy_buffering off;
24
+ proxy_cache off;
25
+ # 这一行可以确保 Nginx 不再做加速层的缓冲
26
+ proxy_set_header X-Accel-Buffering off;
27
+
28
+ # 打开分块传输
29
+ chunked_transfer_encoding on;
30
+
31
+ proxy_read_timeout 3600s;
32
+ proxy_send_timeout 3600s;
33
+ }
34
+
35
+ gzip on;
36
+ # 不包含 text/event-stream
37
+ gzip_types text/plain application/json;
38
+ }
39
+ }