github-actions[bot] commited on
Commit
216f5cb
·
1 Parent(s): 674d314

Update from GitHub Actions

Browse files
Files changed (21) hide show
  1. .env.example +10 -0
  2. .gitattributes +2 -0
  3. Dockerfile +27 -0
  4. LICENSE +21 -0
  5. config/config.go +156 -0
  6. config/model.go +49 -0
  7. core/api.go +610 -0
  8. go.mod +57 -0
  9. go.sum +140 -0
  10. job/cookie.go +211 -0
  11. logger/logger.go +104 -0
  12. main.go +27 -0
  13. middleware/auth.go +31 -0
  14. middleware/cors.go +17 -0
  15. model/openai.go +130 -0
  16. router/router.go +30 -0
  17. service/handle.go +170 -0
  18. utils/imageShow.go +8 -0
  19. utils/random.go +12 -0
  20. utils/role.go +22 -0
  21. utils/searchShow.go +26 -0
.env.example ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ SESSIONS=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0**,eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0**
2
+ ADDRESS=0.0.0.0:8080
3
+ APIKEY=123
4
+ IS_INCOGNITO=true
5
+ PROXY=http://127.0.0.1:2080
6
+ MAX_CHAT_HISTORY_LENGTH=10000
7
+ NO_ROLE_PREFIX=false
8
+ SEARCH_RESULT_COMPATIBLE=false
9
+ PROMPT_FOR_FILE=You must immerse yourself in the role of assistant in txt file, cannot respond as a user, cannot reply to this message, cannot mention this message, and ignore this message in your response.
10
+ IGNORE_SEARCH_RESULT=false
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
37
+ *.webp filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Start from the official Golang image
2
+ FROM golang:1.23-alpine AS build
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy go.mod and go.sum files first for better caching
8
+ COPY go.mod go.sum* ./
9
+
10
+ # Download dependencies
11
+ RUN go mod download
12
+
13
+ # Copy the source code
14
+ COPY . .
15
+
16
+ # Build the application
17
+ RUN CGO_ENABLED=0 GOOS=linux go build -o main ./main.go
18
+
19
+ # Create a minimal production image
20
+ FROM alpine:latest
21
+
22
+ # Create app directory and set permissions
23
+ WORKDIR /app
24
+ COPY --from=build /app/main .
25
+
26
+ # Command to run the executable
27
+ CMD ["./main"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 yuxiao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
config/config.go ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "fmt"
5
+ "math/rand"
6
+ "os"
7
+ "pplx2api/logger"
8
+ "strconv"
9
+ "strings"
10
+ "sync"
11
+ "time"
12
+
13
+ "github.com/joho/godotenv"
14
+ )
15
+
16
+ type SessionInfo struct {
17
+ SessionKey string
18
+ }
19
+
20
+ type SessionRagen struct {
21
+ Index int
22
+ Mutex sync.Mutex
23
+ }
24
+
25
+ type Config struct {
26
+ Sessions []SessionInfo
27
+ Address string
28
+ APIKey string
29
+ Proxy string
30
+ IsIncognito bool
31
+ MaxChatHistoryLength int
32
+ RetryCount int
33
+ NoRolePrefix bool
34
+ SearchResultCompatible bool
35
+ PromptForFile string
36
+ RwMutex sync.RWMutex
37
+ IgnoreSerchResult bool
38
+ IgnoreModelMonitoring bool
39
+ }
40
+
41
+ // 解析 SESSION 格式的环境变量
42
+ func parseSessionEnv(envValue string) (int, []SessionInfo) {
43
+ if envValue == "" {
44
+ return 0, []SessionInfo{}
45
+ }
46
+ var sessions []SessionInfo
47
+ sessionPairs := strings.Split(envValue, ",")
48
+ retryCount := len(sessionPairs) // 重试次数等于 session 数量
49
+ for _, pair := range sessionPairs {
50
+ if pair == "" {
51
+ retryCount--
52
+ continue
53
+ }
54
+ parts := strings.Split(pair, ":")
55
+ session := SessionInfo{
56
+ SessionKey: parts[0],
57
+ }
58
+ sessions = append(sessions, session)
59
+ }
60
+ return retryCount, sessions
61
+ }
62
+
63
+ // 根据模型选择合适的 session
64
+ func (c *Config) GetSessionForModel(idx int) (SessionInfo, error) {
65
+ if len(c.Sessions) == 0 || idx < 0 || idx >= len(c.Sessions) {
66
+ return SessionInfo{}, fmt.Errorf("invalid session index: %d", idx)
67
+ }
68
+ c.RwMutex.RLock()
69
+ defer c.RwMutex.RUnlock()
70
+ return c.Sessions[idx], nil
71
+ }
72
+
73
+ // 从环境变量加载配置
74
+ func LoadConfig() *Config {
75
+ maxChatHistoryLength, err := strconv.Atoi(os.Getenv("MAX_CHAT_HISTORY_LENGTH"))
76
+ if err != nil {
77
+ maxChatHistoryLength = 10000 // 默认值
78
+ }
79
+ retryCount, sessions := parseSessionEnv(os.Getenv("SESSIONS"))
80
+ promptForFile := os.Getenv("PROMPT_FOR_FILE")
81
+ if promptForFile == "" {
82
+ promptForFile = "You must immerse yourself in the role of assistant in txt file, cannot respond as a user, cannot reply to this message, cannot mention this message, and ignore this message in your response." // 默认值
83
+ }
84
+ config := &Config{
85
+ // 解析 SESSIONS 环境变量
86
+ Sessions: sessions,
87
+ // 设置服务地址,默认为 "0.0.0.0:8080"
88
+ Address: os.Getenv("ADDRESS"),
89
+
90
+ // 设置 API 认证密钥
91
+ APIKey: os.Getenv("APIKEY"),
92
+ // 设置代理地址
93
+ Proxy: os.Getenv("PROXY"),
94
+ //是否匿名
95
+ IsIncognito: os.Getenv("IS_INCOGNITO") != "false",
96
+ // 设置最大聊天历史长度
97
+ MaxChatHistoryLength: maxChatHistoryLength,
98
+ // 设置重试次数
99
+ RetryCount: retryCount,
100
+ // 设置是否使用角色前缀
101
+ NoRolePrefix: os.Getenv("NO_ROLE_PREFIX") == "true",
102
+ // 设置搜索结果兼容性
103
+ SearchResultCompatible: os.Getenv("SEARCH_RESULT_COMPATIBLE") == "true",
104
+ // 设置上传文件后的提示词
105
+ PromptForFile: promptForFile,
106
+ // 设置是否忽略搜索结果
107
+ IgnoreSerchResult: os.Getenv("IGNORE_SEARCH_RESULT") == "true",
108
+ //设置是否忽略模型监控
109
+ IgnoreModelMonitoring: os.Getenv("IGNORE_MODEL_MONITORING") == "true",
110
+ // 读写锁
111
+ RwMutex: sync.RWMutex{},
112
+ }
113
+
114
+ // 如果地址为空,使用默认值
115
+ if config.Address == "" {
116
+ config.Address = "0.0.0.0:8080"
117
+ }
118
+ return config
119
+ }
120
+
121
+ var ConfigInstance *Config
122
+ var Sr *SessionRagen
123
+
124
+ func (sr *SessionRagen) NextIndex() int {
125
+ sr.Mutex.Lock()
126
+ defer sr.Mutex.Unlock()
127
+
128
+ index := sr.Index
129
+ sr.Index = (index + 1) % len(ConfigInstance.Sessions)
130
+ return index
131
+ }
132
+ func init() {
133
+ rand.Seed(time.Now().UnixNano())
134
+ // 加载环境变量
135
+ _ = godotenv.Load()
136
+ Sr = &SessionRagen{
137
+ Index: 0,
138
+ Mutex: sync.Mutex{},
139
+ }
140
+ ConfigInstance = LoadConfig()
141
+ logger.Info("Loaded config:")
142
+ logger.Info(fmt.Sprintf("Sessions count: %d", ConfigInstance.RetryCount))
143
+ for _, session := range ConfigInstance.Sessions {
144
+ logger.Info(fmt.Sprintf("Session: %s", session.SessionKey))
145
+ }
146
+ logger.Info(fmt.Sprintf("Address: %s", ConfigInstance.Address))
147
+ logger.Info(fmt.Sprintf("APIKey: %s", ConfigInstance.APIKey))
148
+ logger.Info(fmt.Sprintf("Proxy: %s", ConfigInstance.Proxy))
149
+ logger.Info(fmt.Sprintf("IsIncognito: %t", ConfigInstance.IsIncognito))
150
+ logger.Info(fmt.Sprintf("MaxChatHistoryLength: %d", ConfigInstance.MaxChatHistoryLength))
151
+ logger.Info(fmt.Sprintf("NoRolePrefix: %t", ConfigInstance.NoRolePrefix))
152
+ logger.Info(fmt.Sprintf("SearchResultCompatible: %t", ConfigInstance.SearchResultCompatible))
153
+ logger.Info(fmt.Sprintf("PromptForFile: %s", ConfigInstance.PromptForFile))
154
+ logger.Info(fmt.Sprintf("IgnoreSerchResult: %t", ConfigInstance.IgnoreSerchResult))
155
+ logger.Info(fmt.Sprintf("IgnoreModelMonitoring: %t", ConfigInstance.IgnoreModelMonitoring))
156
+ }
config/model.go ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ var ModelReverseMap = map[string]string{}
4
+ var ModelMap = map[string]string{
5
+ "claude-4.0-sonnet": "claude2",
6
+ "claude-4.0-sonnet-think": "claude37sonnetthinking",
7
+ "deepseek-r1": "r1",
8
+ "o4-mini": "o4mini",
9
+ "gpt-4o": "gpt4o",
10
+ "gemini-2.5-pro-06-05": "gemini2flash",
11
+ "grok-3-beta": "grok",
12
+ "gpt-4.1": "gpt41",
13
+ // "claude-4.0-opus": "claude40opus",
14
+ // "claude-4.0-opus-think": "claude40opusthinking",
15
+ "o3": "o3",
16
+ }
17
+
18
+ // Get returns the value for the given key from the ModelMap.
19
+ // If the key doesn't exist, it returns the provided default value.
20
+ func ModelMapGet(key string, defaultValue string) string {
21
+ if value, exists := ModelMap[key]; exists {
22
+ return value
23
+ }
24
+ return defaultValue
25
+ }
26
+
27
+ // GetReverse returns the value for the given key from the ModelReverseMap.
28
+ // If the key doesn't exist, it returns the provided default value.
29
+ func ModelReverseMapGet(key string, defaultValue string) string {
30
+ if value, exists := ModelReverseMap[key]; exists {
31
+ return value
32
+ }
33
+ return defaultValue
34
+ }
35
+
36
+ var ResponseModles []map[string]string
37
+
38
+ func init() {
39
+ for k, v := range ModelMap {
40
+ ModelReverseMap[v] = k
41
+ model := map[string]string{
42
+ "id": k,
43
+ }
44
+ modelSearch := map[string]string{
45
+ "id": k + "-search",
46
+ }
47
+ ResponseModles = append(ResponseModles, model, modelSearch)
48
+ }
49
+ }
core/api.go ADDED
@@ -0,0 +1,610 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package core
2
+
3
+ import (
4
+ "bufio"
5
+ "bytes"
6
+ "encoding/base64"
7
+ "encoding/json"
8
+ "fmt"
9
+ "io"
10
+ "mime/multipart"
11
+ "net/http"
12
+ "pplx2api/config"
13
+ "pplx2api/logger"
14
+ "pplx2api/model"
15
+ "pplx2api/utils"
16
+ "strings"
17
+ "time"
18
+
19
+ "github.com/gin-gonic/gin"
20
+ "github.com/google/uuid"
21
+ "github.com/imroc/req/v3"
22
+ )
23
+
24
+ // Client represents a Perplexity API client
25
+ type Client struct {
26
+ sessionToken string
27
+ client *req.Client
28
+ Model string
29
+ Attachments []string
30
+ OpenSerch bool
31
+ }
32
+
33
+ // Perplexity API structures
34
+ type PerplexityRequest struct {
35
+ Params PerplexityParams `json:"params"`
36
+ QueryStr string `json:"query_str"`
37
+ }
38
+
39
+ type PerplexityParams struct {
40
+ Attachments []string `json:"attachments"`
41
+ Language string `json:"language"`
42
+ Timezone string `json:"timezone"`
43
+ SearchFocus string `json:"search_focus"`
44
+ Sources []string `json:"sources"`
45
+ SearchRecencyFilter interface{} `json:"search_recency_filter"`
46
+ FrontendUUID string `json:"frontend_uuid"`
47
+ Mode string `json:"mode"`
48
+ ModelPreference string `json:"model_preference"`
49
+ IsRelatedQuery bool `json:"is_related_query"`
50
+ IsSponsored bool `json:"is_sponsored"`
51
+ VisitorID string `json:"visitor_id"`
52
+ UserNextauthID string `json:"user_nextauth_id"`
53
+ FrontendContextUUID string `json:"frontend_context_uuid"`
54
+ PromptSource string `json:"prompt_source"`
55
+ QuerySource string `json:"query_source"`
56
+ BrowserHistorySummary []interface{} `json:"browser_history_summary"`
57
+ IsIncognito bool `json:"is_incognito"`
58
+ UseSchematizedAPI bool `json:"use_schematized_api"`
59
+ SendBackTextInStreaming bool `json:"send_back_text_in_streaming_api"`
60
+ SupportedBlockUseCases []string `json:"supported_block_use_cases"`
61
+ ClientCoordinates interface{} `json:"client_coordinates"`
62
+ IsNavSuggestionsDisabled bool `json:"is_nav_suggestions_disabled"`
63
+ Version string `json:"version"`
64
+ }
65
+
66
+ // Response structures
67
+ type PerplexityResponse struct {
68
+ Blocks []Block `json:"blocks"`
69
+ Status string `json:"status"`
70
+ DisplayModel string `json:"display_model"`
71
+ }
72
+
73
+ type Block struct {
74
+ MarkdownBlock *MarkdownBlock `json:"markdown_block,omitempty"`
75
+ ReasoningPlanBlock *ReasoningPlanBlock `json:"reasoning_plan_block,omitempty"`
76
+ WebResultBlock *WebResultBlock `json:"web_result_block,omitempty"`
77
+ ImageModeBlock *ImageModeBlock `json:"image_mode_block,omitempty"`
78
+ }
79
+
80
+ type MarkdownBlock struct {
81
+ Chunks []string `json:"chunks"`
82
+ }
83
+
84
+ type ReasoningPlanBlock struct {
85
+ Goals []Goal `json:"goals"`
86
+ }
87
+
88
+ type Goal struct {
89
+ Description string `json:"description"`
90
+ }
91
+
92
+ type WebResultBlock struct {
93
+ WebResults []WebResult `json:"web_results"`
94
+ }
95
+
96
+ type WebResult struct {
97
+ Name string `json:"name"`
98
+ Snippet string `json:"snippet"`
99
+ URL string `json:"url"`
100
+ }
101
+
102
+ type ImageModeBlock struct {
103
+ AnswerModeType string `json:"answer_mode_type"`
104
+ Progress string `json:"progress"`
105
+ MediaItems []struct {
106
+ Medium string `json:"medium"`
107
+ Image string `json:"image"`
108
+ URL string `json:"url"`
109
+ Name string `json:"name"`
110
+ Source string `json:"source"`
111
+ Thumbnail string `json:"thumbnail"`
112
+ } `json:"media_items"`
113
+ }
114
+
115
+ // NewClient creates a new Perplexity API client
116
+ func NewClient(sessionToken string, proxy string, model string, openSerch bool) *Client {
117
+ client := req.C().ImpersonateChrome().SetTimeout(time.Minute * 10)
118
+ client.Transport.SetResponseHeaderTimeout(time.Second * 10)
119
+ if proxy != "" {
120
+ client.SetProxyURL(proxy)
121
+ }
122
+
123
+ // Set common headers
124
+ headers := map[string]string{
125
+ "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6",
126
+ "cache-control": "no-cache",
127
+ "origin": "https://www.perplexity.ai",
128
+ "pragma": "no-cache",
129
+ "priority": "u=1, i",
130
+ "referer": "https://www.perplexity.ai/",
131
+ }
132
+
133
+ for key, value := range headers {
134
+ client.SetCommonHeader(key, value)
135
+ }
136
+
137
+ // Set cookies
138
+ if sessionToken != "" {
139
+ client.SetCommonCookies(&http.Cookie{
140
+ Name: "__Secure-next-auth.session-token",
141
+ Value: sessionToken,
142
+ })
143
+ }
144
+
145
+ // Create client with visitor ID
146
+ c := &Client{
147
+ sessionToken: sessionToken,
148
+ client: client,
149
+ Model: model,
150
+ Attachments: []string{},
151
+ OpenSerch: openSerch,
152
+ }
153
+
154
+ return c
155
+ }
156
+
157
+ // SendMessage sends a message to Perplexity and returns the status and response
158
+ func (c *Client) SendMessage(message string, stream bool, is_incognito bool, gc *gin.Context) (int, error) {
159
+ // Create request body
160
+ requestBody := PerplexityRequest{
161
+ Params: PerplexityParams{
162
+ Attachments: c.Attachments,
163
+ Language: "en-US",
164
+ Timezone: "America/New_York",
165
+ SearchFocus: "writing",
166
+ Sources: []string{},
167
+ // SearchFocus: "internet",
168
+ // Sources: []string{"web"},
169
+ SearchRecencyFilter: nil,
170
+ FrontendUUID: uuid.New().String(),
171
+ Mode: "copilot",
172
+ ModelPreference: c.Model,
173
+ IsRelatedQuery: false,
174
+ IsSponsored: false,
175
+ VisitorID: uuid.New().String(),
176
+ UserNextauthID: uuid.New().String(),
177
+ FrontendContextUUID: uuid.New().String(),
178
+ PromptSource: "user",
179
+ QuerySource: "home",
180
+ BrowserHistorySummary: []interface{}{},
181
+ IsIncognito: is_incognito,
182
+ UseSchematizedAPI: true,
183
+ SendBackTextInStreaming: false,
184
+ SupportedBlockUseCases: []string{
185
+ "answer_modes",
186
+ "media_items",
187
+ "knowledge_cards",
188
+ "inline_entity_cards",
189
+ "place_widgets",
190
+ "finance_widgets",
191
+ "sports_widgets",
192
+ "shopping_widgets",
193
+ "jobs_widgets",
194
+ "search_result_widgets",
195
+ "entity_list_answer",
196
+ "todo_list",
197
+ },
198
+ ClientCoordinates: nil,
199
+ IsNavSuggestionsDisabled: false,
200
+ Version: "2.18",
201
+ },
202
+ QueryStr: message,
203
+ }
204
+ if c.OpenSerch {
205
+ requestBody.Params.SearchFocus = "internet"
206
+ requestBody.Params.Sources = append(requestBody.Params.Sources, "web")
207
+ }
208
+ logger.Info(fmt.Sprintf("Perplexity request body: %v", requestBody))
209
+ // Make the request
210
+ resp, err := c.client.R().DisableAutoReadResponse().
211
+ SetBody(requestBody).
212
+ Post("https://www.perplexity.ai/rest/sse/perplexity_ask")
213
+
214
+ if err != nil {
215
+ logger.Error(fmt.Sprintf("Error sending request: %v", err))
216
+ return 500, fmt.Errorf("request failed: %w", err)
217
+ }
218
+
219
+ logger.Info(fmt.Sprintf("Perplexity response status code: %d", resp.StatusCode))
220
+
221
+ if resp.StatusCode == http.StatusTooManyRequests {
222
+ resp.Body.Close()
223
+ return http.StatusTooManyRequests, fmt.Errorf("rate limit exceeded")
224
+ }
225
+
226
+ if resp.StatusCode != http.StatusOK {
227
+ logger.Error(fmt.Sprintf("Unexpected return data: %s", resp.String()))
228
+ resp.Body.Close()
229
+ return resp.StatusCode, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
230
+ }
231
+
232
+ return 200, c.HandleResponse(resp.Body, stream, gc)
233
+ }
234
+
235
+ func (c *Client) HandleResponse(body io.ReadCloser, stream bool, gc *gin.Context) error {
236
+ defer body.Close()
237
+ // Set headers for streaming
238
+ if stream {
239
+ gc.Writer.Header().Set("Content-Type", "text/event-stream")
240
+ gc.Writer.Header().Set("Cache-Control", "no-cache")
241
+ gc.Writer.Header().Set("Connection", "keep-alive")
242
+ gc.Writer.WriteHeader(http.StatusOK)
243
+ gc.Writer.Flush()
244
+ }
245
+ scanner := bufio.NewScanner(body)
246
+ clientDone := gc.Request.Context().Done()
247
+ // 增大缓冲区大小
248
+ scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
249
+ full_text := ""
250
+ inThinking := false
251
+ thinkShown := false
252
+ final := false
253
+ for scanner.Scan() {
254
+ select {
255
+ case <-clientDone:
256
+ logger.Info("Client connection closed")
257
+ return nil
258
+ default:
259
+ }
260
+
261
+ line := scanner.Text()
262
+ // Skip empty lines
263
+ if line == "" {
264
+ continue
265
+ }
266
+ if !strings.HasPrefix(line, "data: ") {
267
+ continue
268
+ }
269
+ data := line[6:]
270
+ // logger.Info(fmt.Sprintf("Received data: %s", data))
271
+ var response PerplexityResponse
272
+ if err := json.Unmarshal([]byte(data), &response); err != nil {
273
+ logger.Error(fmt.Sprintf("Error parsing JSON: %v", err))
274
+ continue
275
+ }
276
+ // Check for completion and web results
277
+ if response.Status == "COMPLETED" {
278
+ final = true
279
+ for _, block := range response.Blocks {
280
+ if block.ImageModeBlock != nil && block.ImageModeBlock.Progress == "DONE" && len(block.ImageModeBlock.MediaItems) > 0 {
281
+ imageResultsText := ""
282
+ imageModelList := []string{}
283
+ for i, result := range block.ImageModeBlock.MediaItems {
284
+ imageResultsText += utils.ImageShow(i, result.Name, result.Image)
285
+ imageModelList = append(imageModelList, result.Name)
286
+
287
+ }
288
+ if len(imageModelList) > 0 {
289
+ imageResultsText = imageResultsText + "\n\n---\n" + strings.Join(imageModelList, ", ")
290
+ }
291
+ full_text += imageResultsText
292
+
293
+ if stream {
294
+ model.ReturnOpenAIResponse(imageResultsText, stream, gc)
295
+ }
296
+ }
297
+ }
298
+ for _, block := range response.Blocks {
299
+ if !config.ConfigInstance.IgnoreSerchResult && block.WebResultBlock != nil && len(block.WebResultBlock.WebResults) > 0 {
300
+ webResultsText := "\n\n---\n"
301
+ for i, result := range block.WebResultBlock.WebResults {
302
+ webResultsText += "\n\n" + utils.SearchShow(i, result.Name, result.URL, result.Snippet)
303
+ }
304
+ full_text += webResultsText
305
+
306
+ if stream {
307
+ model.ReturnOpenAIResponse(webResultsText, stream, gc)
308
+ }
309
+ }
310
+
311
+ }
312
+
313
+ if !config.ConfigInstance.IgnoreModelMonitoring && response.DisplayModel != c.Model {
314
+ res_text := "\n\n---\n"
315
+ res_text += fmt.Sprintf("Display Model: %s\n", config.ModelReverseMapGet(response.DisplayModel, response.DisplayModel))
316
+ full_text += res_text
317
+ if !stream {
318
+ break
319
+ }
320
+ model.ReturnOpenAIResponse(res_text, stream, gc)
321
+ }
322
+ }
323
+ if final {
324
+ break
325
+ }
326
+ // Process each block in the response
327
+ for _, block := range response.Blocks {
328
+ // Handle reasoning plan blocks (thinking)
329
+ if block.ReasoningPlanBlock != nil && len(block.ReasoningPlanBlock.Goals) > 0 {
330
+
331
+ res_text := ""
332
+ if !inThinking && !thinkShown {
333
+ res_text += "<think>"
334
+ inThinking = true
335
+ }
336
+
337
+ for _, goal := range block.ReasoningPlanBlock.Goals {
338
+ if goal.Description != "" && goal.Description != "Beginning analysis" && goal.Description != "Wrapping up analysis" {
339
+ res_text += goal.Description
340
+ }
341
+ }
342
+ full_text += res_text
343
+ if !stream {
344
+ continue
345
+ }
346
+ model.ReturnOpenAIResponse(res_text, stream, gc)
347
+ }
348
+ }
349
+ for _, block := range response.Blocks {
350
+ if block.MarkdownBlock != nil && len(block.MarkdownBlock.Chunks) > 0 {
351
+ res_text := ""
352
+ if inThinking {
353
+ res_text += "</think>\n"
354
+ inThinking = false
355
+ thinkShown = true
356
+ }
357
+ for _, chunk := range block.MarkdownBlock.Chunks {
358
+ if chunk != "" {
359
+ res_text += chunk
360
+ }
361
+ }
362
+ full_text += res_text
363
+ if !stream {
364
+ continue
365
+ }
366
+ model.ReturnOpenAIResponse(res_text, stream, gc)
367
+ }
368
+ }
369
+
370
+ }
371
+
372
+ if err := scanner.Err(); err != nil {
373
+ return fmt.Errorf("error reading response: %w", err)
374
+ }
375
+
376
+ if !stream {
377
+ model.ReturnOpenAIResponse(full_text, stream, gc)
378
+ } else {
379
+ // Send end marker for streaming mode
380
+ gc.Writer.Write([]byte("data: [DONE]\n\n"))
381
+ gc.Writer.Flush()
382
+ }
383
+
384
+ return nil
385
+ }
386
+
387
+ // UploadURLResponse represents the response from the create_upload_url endpoint
388
+ type UploadURLResponse struct {
389
+ S3BucketURL string `json:"s3_bucket_url"`
390
+ S3ObjectURL string `json:"s3_object_url"`
391
+ Fields CloudinaryUploadInfo `json:"fields"`
392
+ RateLimited bool `json:"rate_limited"`
393
+ }
394
+
395
+ type CloudinaryUploadInfo struct {
396
+ Timestamp int `json:"timestamp"`
397
+ UniqueFilename string `json:"unique_filename"`
398
+ Folder string `json:"folder"`
399
+ UseFilename string `json:"use_filename"`
400
+ PublicID string `json:"public_id"`
401
+ Transformation string `json:"transformation"`
402
+ Moderation string `json:"moderation"`
403
+ ResourceType string `json:"resource_type"`
404
+ APIKey string `json:"api_key"`
405
+ CloudName string `json:"cloud_name"`
406
+ Signature string `json:"signature"`
407
+ AWSAccessKeyId string `json:"AWSAccessKeyId"`
408
+ Key string `json:"key"`
409
+ Tagging string `json:"tagging"`
410
+ Policy string `json:"policy"`
411
+ Xamzsecuritytoken string `json:"x-amz-security-token"`
412
+ ACL string `json:"acl"`
413
+ }
414
+
415
+ // UploadFile is a placeholder for file upload functionality
416
+ func (c *Client) createUploadURL(filename string, contentType string) (*UploadURLResponse, error) {
417
+ requestBody := map[string]interface{}{
418
+ "filename": filename,
419
+ "content_type": contentType,
420
+ "source": "default",
421
+ "file_size": 12000,
422
+ "force_image": false,
423
+ }
424
+ resp, err := c.client.R().
425
+ SetBody(requestBody).
426
+ Post("https://www.perplexity.ai/rest/uploads/create_upload_url?version=2.18&source=default")
427
+ if err != nil {
428
+ logger.Error(fmt.Sprintf("Error creating upload URL: %v", err))
429
+ return nil, err
430
+ }
431
+ if resp.StatusCode != http.StatusOK {
432
+ logger.Error(fmt.Sprintf("Image Upload with status code %d: %s", resp.StatusCode, resp.String()))
433
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
434
+ }
435
+ var uploadURLResponse UploadURLResponse
436
+ logger.Info(fmt.Sprintf("Create upload with status code %d: %s", resp.StatusCode, resp.String()))
437
+ if err := json.Unmarshal(resp.Bytes(), &uploadURLResponse); err != nil {
438
+ logger.Error(fmt.Sprintf("Error unmarshalling upload URL response: %v", err))
439
+ return nil, err
440
+ }
441
+ if uploadURLResponse.RateLimited {
442
+ logger.Error("Rate limit exceeded for upload URL")
443
+ return nil, fmt.Errorf("rate limit exceeded")
444
+ }
445
+ return &uploadURLResponse, nil
446
+
447
+ }
448
+
449
+ func (c *Client) UploadImage(img_list []string) error {
450
+ logger.Info(fmt.Sprintf("Uploading %d images to Cloudinary", len(img_list)))
451
+
452
+ // Upload images to Cloudinary
453
+ for _, img := range img_list {
454
+ filename := utils.RandomString(5) + ".jpg"
455
+ // Create upload URL
456
+ uploadURLResponse, err := c.createUploadURL(filename, "image/jpeg")
457
+ if err != nil {
458
+ logger.Error(fmt.Sprintf("Error creating upload URL: %v", err))
459
+ return err
460
+ }
461
+ logger.Info(fmt.Sprintf("Upload URL response: %v", uploadURLResponse))
462
+ // Upload image to Cloudinary
463
+ err = c.UloadFileToCloudinary(uploadURLResponse.Fields, "img", img, filename)
464
+ if err != nil {
465
+ logger.Error(fmt.Sprintf("Error uploading image: %v", err))
466
+ return err
467
+ }
468
+ }
469
+ return nil
470
+ }
471
+
472
+ func (c *Client) UloadFileToCloudinary(uploadInfo CloudinaryUploadInfo, contentType string, filedata string, filename string) error {
473
+ if len(filedata) > 100 {
474
+ logger.Info(fmt.Sprintf("filedata: %s ……", filedata[:50]))
475
+ }
476
+ // Add form fields
477
+ logger.Info(fmt.Sprintf("Uploading file %s to Cloudinary", filename))
478
+ var formFields map[string]string
479
+ if contentType == "img" {
480
+ formFields = map[string]string{
481
+ "timestamp": fmt.Sprintf("%d", uploadInfo.Timestamp),
482
+ "unique_filename": uploadInfo.UniqueFilename,
483
+ "folder": uploadInfo.Folder,
484
+ "use_filename": uploadInfo.UseFilename,
485
+ "public_id": uploadInfo.PublicID,
486
+ "transformation": uploadInfo.Transformation,
487
+ "moderation": uploadInfo.Moderation,
488
+ "resource_type": uploadInfo.ResourceType,
489
+ "api_key": uploadInfo.APIKey,
490
+ "cloud_name": uploadInfo.CloudName,
491
+ "signature": uploadInfo.Signature,
492
+ "type": "private",
493
+ }
494
+ } else {
495
+ formFields = map[string]string{
496
+ "acl": uploadInfo.ACL,
497
+ "Content-Type": "text/plain",
498
+ "tagging": uploadInfo.Tagging,
499
+ "key": uploadInfo.Key,
500
+ "AWSAccessKeyId": uploadInfo.AWSAccessKeyId,
501
+ "x-amz-security-token": uploadInfo.Xamzsecuritytoken,
502
+ "policy": uploadInfo.Policy,
503
+ "signature": uploadInfo.Signature,
504
+ }
505
+ }
506
+ var requestBody bytes.Buffer
507
+ writer := multipart.NewWriter(&requestBody)
508
+ for key, value := range formFields {
509
+ if err := writer.WriteField(key, value); err != nil {
510
+ logger.Error(fmt.Sprintf("Error writing form field %s: %v", key, err))
511
+ return err
512
+ }
513
+ }
514
+
515
+ // Add the file,filedata 是base64编码的字符串
516
+ decodedData, err := base64.StdEncoding.DecodeString(filedata)
517
+ if err != nil {
518
+ logger.Error(fmt.Sprintf("Error decoding base64 data: %v", err))
519
+ return err
520
+ }
521
+
522
+ // 创建一个文件部分
523
+ part, err := writer.CreateFormFile("file", filename) // 替换 filename.ext 为实际文件名
524
+ if err != nil {
525
+ logger.Error(fmt.Sprintf("Error creating form file: %v", err))
526
+ return err
527
+ }
528
+
529
+ // 将解码后的数据写入文件部分
530
+ if _, err := part.Write(decodedData); err != nil {
531
+ logger.Error(fmt.Sprintf("Error writing file data: %v", err))
532
+ return err
533
+ }
534
+ // Close the writer to finalize the form
535
+ if err := writer.Close(); err != nil {
536
+ logger.Error(fmt.Sprintf("Error closing writer: %v", err))
537
+ return err
538
+ }
539
+
540
+ // Create the upload request
541
+ var uploadURL string
542
+ if contentType == "img" {
543
+ uploadURL = fmt.Sprintf("https://api.cloudinary.com/v1_1/%s/image/upload", uploadInfo.CloudName)
544
+ } else {
545
+ uploadURL = "https://ppl-ai-file-upload.s3.amazonaws.com/"
546
+ }
547
+
548
+ resp, err := c.client.R().
549
+ SetHeader("Content-Type", writer.FormDataContentType()).
550
+ SetBodyBytes(requestBody.Bytes()).
551
+ Post(uploadURL)
552
+
553
+ if err != nil {
554
+ logger.Error(fmt.Sprintf("Error uploading file: %v", err))
555
+ return err
556
+ }
557
+ logger.Info(fmt.Sprintf("Image Upload with status code %d: %s", resp.StatusCode, resp.String()))
558
+ if contentType == "img" {
559
+ var uploadResponse map[string]interface{}
560
+ if err := json.Unmarshal(resp.Bytes(), &uploadResponse); err != nil {
561
+ return err
562
+ }
563
+ imgUrl := uploadResponse["secure_url"].(string)
564
+ imgUrl = "https://pplx-res.cloudinary.com/image/private" + imgUrl[strings.Index(imgUrl, "/user_uploads"):]
565
+ c.Attachments = append(c.Attachments, imgUrl)
566
+ } else {
567
+ c.Attachments = append(c.Attachments, "https://ppl-ai-file-upload.s3.amazonaws.com/"+uploadInfo.Key)
568
+ }
569
+ return nil
570
+ }
571
+
572
+ // SetBigContext is a placeholder for setting context
573
+ func (c *Client) UploadText(context string) error {
574
+ logger.Info("Uploading txt to Cloudinary")
575
+ filedata := base64.StdEncoding.EncodeToString([]byte(context))
576
+ filename := utils.RandomString(5) + ".txt"
577
+ // Upload images to Cloudinary
578
+ uploadURLResponse, err := c.createUploadURL(filename, "text/plain")
579
+ if err != nil {
580
+ logger.Error(fmt.Sprintf("Error creating upload URL: %v", err))
581
+ return err
582
+ }
583
+ logger.Info(fmt.Sprintf("Upload URL response: %v", uploadURLResponse))
584
+ // Upload txt to Cloudinary
585
+ err = c.UloadFileToCloudinary(uploadURLResponse.Fields, "txt", filedata, filename)
586
+ if err != nil {
587
+ logger.Error(fmt.Sprintf("Error uploading image: %v", err))
588
+ return err
589
+ }
590
+
591
+ return nil
592
+ }
593
+
594
+ func (c *Client) GetNewCookie() (string, error) {
595
+ resp, err := c.client.R().Get("https://www.perplexity.ai/api/auth/session")
596
+ if err != nil {
597
+ logger.Error(fmt.Sprintf("Error getting session cookie: %v", err))
598
+ return "", err
599
+ }
600
+ if resp.StatusCode != http.StatusOK {
601
+ logger.Error(fmt.Sprintf("Error getting session cookie: %s", resp.String()))
602
+ return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
603
+ }
604
+ for _, cookie := range resp.Cookies() {
605
+ if cookie.Name == "__Secure-next-auth.session-token" {
606
+ return cookie.Value, nil
607
+ }
608
+ }
609
+ return "", fmt.Errorf("session cookie not found")
610
+ }
go.mod ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module pplx2api
2
+
3
+ go 1.22.2
4
+
5
+ require (
6
+ github.com/fatih/color v1.18.0
7
+ github.com/gin-gonic/gin v1.10.0
8
+ github.com/google/uuid v1.6.0
9
+ github.com/imroc/req/v3 v3.50.0
10
+ github.com/joho/godotenv v1.5.1
11
+ )
12
+
13
+ require (
14
+ github.com/andybalholm/brotli v1.1.1 // indirect
15
+ github.com/bytedance/sonic v1.11.6 // indirect
16
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
17
+ github.com/cloudflare/circl v1.5.0 // indirect
18
+ github.com/cloudwego/base64x v0.1.4 // indirect
19
+ github.com/cloudwego/iasm v0.2.0 // indirect
20
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
21
+ github.com/gin-contrib/sse v0.1.0 // indirect
22
+ github.com/go-playground/locales v0.14.1 // indirect
23
+ github.com/go-playground/universal-translator v0.18.1 // indirect
24
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
25
+ github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
26
+ github.com/goccy/go-json v0.10.2 // indirect
27
+ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
28
+ github.com/hashicorp/errwrap v1.1.0 // indirect
29
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
30
+ github.com/json-iterator/go v1.1.12 // indirect
31
+ github.com/klauspost/compress v1.17.11 // indirect
32
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
33
+ github.com/leodido/go-urn v1.4.0 // indirect
34
+ github.com/mattn/go-colorable v0.1.13 // indirect
35
+ github.com/mattn/go-isatty v0.0.20 // indirect
36
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
37
+ github.com/modern-go/reflect2 v1.0.2 // indirect
38
+ github.com/onsi/ginkgo/v2 v2.22.0 // indirect
39
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
40
+ github.com/quic-go/qpack v0.5.1 // indirect
41
+ github.com/quic-go/quic-go v0.48.2 // indirect
42
+ github.com/refraction-networking/utls v1.6.7 // indirect
43
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
44
+ github.com/ugorji/go/codec v1.2.12 // indirect
45
+ go.uber.org/mock v0.5.0 // indirect
46
+ golang.org/x/arch v0.8.0 // indirect
47
+ golang.org/x/crypto v0.31.0 // indirect
48
+ golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
49
+ golang.org/x/mod v0.22.0 // indirect
50
+ golang.org/x/net v0.33.0 // indirect
51
+ golang.org/x/sync v0.10.0 // indirect
52
+ golang.org/x/sys v0.28.0 // indirect
53
+ golang.org/x/text v0.21.0 // indirect
54
+ golang.org/x/tools v0.28.0 // indirect
55
+ google.golang.org/protobuf v1.34.1 // indirect
56
+ gopkg.in/yaml.v3 v3.0.1 // indirect
57
+ )
go.sum ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
2
+ github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
3
+ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
4
+ github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
5
+ github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
6
+ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
7
+ github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
8
+ github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
9
+ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
10
+ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
11
+ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
12
+ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
13
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
15
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16
+ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
17
+ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
18
+ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
19
+ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
20
+ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
21
+ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
22
+ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
23
+ github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
24
+ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
25
+ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
26
+ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
27
+ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
28
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
29
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
30
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
31
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
32
+ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
33
+ github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
34
+ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
35
+ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
36
+ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
37
+ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
38
+ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
39
+ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
40
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
41
+ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
42
+ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
43
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
44
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
45
+ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
46
+ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
47
+ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
48
+ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
49
+ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
50
+ github.com/imroc/req/v3 v3.50.0 h1:n3BVnZiTRpvkN5T1IB79LC/THhFU9iXksNRMH4ZNVaY=
51
+ github.com/imroc/req/v3 v3.50.0/go.mod h1:tsOk8K7zI6cU4xu/VWCZVtq9Djw9IWm4MslKzme5woU=
52
+ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
53
+ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
54
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
55
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
56
+ github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
57
+ github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
58
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
59
+ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
60
+ github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
61
+ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
62
+ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
63
+ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
64
+ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
65
+ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
66
+ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
67
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
68
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
69
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
70
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
71
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
72
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
73
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
74
+ github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
75
+ github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
76
+ github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
77
+ github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
78
+ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
79
+ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
80
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
81
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
82
+ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
83
+ github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
84
+ github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
85
+ github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
86
+ github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
87
+ github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
88
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
89
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
90
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
91
+ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
92
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
93
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
94
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
95
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
96
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
97
+ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
98
+ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
99
+ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
100
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
101
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
102
+ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
103
+ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
104
+ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
105
+ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
106
+ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
107
+ go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
108
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
109
+ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
110
+ golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
111
+ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
112
+ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
113
+ golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4=
114
+ golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
115
+ golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
116
+ golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
117
+ golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
118
+ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
119
+ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
120
+ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
121
+ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122
+ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
123
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
124
+ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
125
+ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
126
+ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
127
+ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
128
+ golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
129
+ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
130
+ golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
131
+ golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
132
+ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
133
+ google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
134
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
135
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
136
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
137
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
138
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
139
+ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
140
+ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
job/cookie.go ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package job
2
+
3
+ import (
4
+ "encoding/json"
5
+ "io/ioutil"
6
+ "log"
7
+ "os"
8
+ "sync"
9
+ "time"
10
+
11
+ "pplx2api/config"
12
+ "pplx2api/core"
13
+ )
14
+
15
+ const (
16
+ // ConfigFileName is the name of the file to store sessions
17
+ ConfigFileName = "sessions.json"
18
+ )
19
+
20
+ var (
21
+ sessionUpdaterInstance *SessionUpdater
22
+ sessionUpdaterOnce sync.Once
23
+ )
24
+
25
+ // SessionConfig represents the structure to be saved to file
26
+ type SessionConfig struct {
27
+ Sessions []config.SessionInfo `json:"sessions"`
28
+ }
29
+
30
+ // SessionUpdater 管理 Perplexity 会话的定时更新
31
+ type SessionUpdater struct {
32
+ interval time.Duration
33
+ stopChan chan struct{}
34
+ isRunning bool
35
+ runningLock sync.Mutex
36
+ configPath string
37
+ }
38
+
39
+ // NewSessionUpdater 创建一个新的会话更新器
40
+ // interval: 更新间隔时间
41
+ func GetSessionUpdater(interval time.Duration) *SessionUpdater {
42
+ sessionUpdaterOnce.Do(func() {
43
+ // 使用当前文件夹下的配置文件
44
+ configPath := ConfigFileName
45
+
46
+ sessionUpdaterInstance = &SessionUpdater{
47
+ interval: interval,
48
+ stopChan: make(chan struct{}),
49
+ isRunning: false,
50
+ configPath: configPath,
51
+ }
52
+ // 初始化时从文件加载会话
53
+ sessionUpdaterInstance.loadSessionsFromFile()
54
+ })
55
+ return sessionUpdaterInstance
56
+ }
57
+
58
+ // loadSessionsFromFile loads sessions from the config file if it exists
59
+ func (su *SessionUpdater) loadSessionsFromFile() {
60
+ // Check if file exists
61
+ if _, err := os.Stat(su.configPath); os.IsNotExist(err) {
62
+ log.Println("No sessions config file found, will create on first update")
63
+ return
64
+ }
65
+
66
+ // Read the file
67
+ data, err := ioutil.ReadFile(su.configPath)
68
+ if err != nil {
69
+ log.Printf("Failed to read sessions config file: %v", err)
70
+ return
71
+ }
72
+
73
+ // Parse the JSON
74
+ var sessionConfig SessionConfig
75
+ if err := json.Unmarshal(data, &sessionConfig); err != nil {
76
+ log.Printf("Failed to parse sessions config file: %v", err)
77
+ return
78
+ }
79
+
80
+ // Update the config with loaded sessions
81
+ config.ConfigInstance.RwMutex.Lock()
82
+ config.ConfigInstance.Sessions = sessionConfig.Sessions
83
+ config.ConfigInstance.RwMutex.Unlock()
84
+
85
+ log.Printf("Loaded %d sessions from config file", len(sessionConfig.Sessions))
86
+ }
87
+
88
+ // saveSessionsToFile saves the current sessions to the config file
89
+ func (su *SessionUpdater) saveSessionsToFile() error {
90
+ // Get current sessions
91
+ config.ConfigInstance.RwMutex.RLock()
92
+ sessionsCopy := make([]config.SessionInfo, len(config.ConfigInstance.Sessions))
93
+ copy(sessionsCopy, config.ConfigInstance.Sessions)
94
+ config.ConfigInstance.RwMutex.RUnlock()
95
+
96
+ // Create config structure
97
+ sessionConfig := SessionConfig{
98
+ Sessions: sessionsCopy,
99
+ }
100
+
101
+ // Convert to JSON
102
+ data, err := json.MarshalIndent(sessionConfig, "", " ")
103
+ if err != nil {
104
+ return err
105
+ }
106
+
107
+ // Write to file
108
+ err = ioutil.WriteFile(su.configPath, data, 0644)
109
+ if err != nil {
110
+ return err
111
+ }
112
+
113
+ log.Printf("Saved %d sessions to sessions.json file", len(sessionsCopy))
114
+ return nil
115
+ }
116
+
117
+ // Start 启动定时更新任务
118
+ func (su *SessionUpdater) Start() {
119
+ su.runningLock.Lock()
120
+ defer su.runningLock.Unlock()
121
+ if su.isRunning {
122
+ log.Println("Session updater is already running")
123
+ return
124
+ }
125
+ su.isRunning = true
126
+ su.stopChan = make(chan struct{})
127
+ go su.runUpdateLoop()
128
+ log.Println("Session updater started with interval:", su.interval)
129
+ }
130
+
131
+ // Stop 停止定时更新任务
132
+ func (su *SessionUpdater) Stop() {
133
+ su.runningLock.Lock()
134
+ defer su.runningLock.Unlock()
135
+ if !su.isRunning {
136
+ log.Println("Session updater is not running")
137
+ return
138
+ }
139
+ close(su.stopChan)
140
+ su.isRunning = false
141
+ log.Println("Session updater stopped")
142
+ }
143
+
144
+ // runUpdateLoop 运行更新循环
145
+ func (su *SessionUpdater) runUpdateLoop() {
146
+ ticker := time.NewTicker(su.interval)
147
+ defer ticker.Stop()
148
+ // 立即执行一次更新
149
+ // su.updateAllSessions()
150
+ for {
151
+ select {
152
+ case <-ticker.C:
153
+ su.updateAllSessions()
154
+ case <-su.stopChan:
155
+ log.Println("Update loop terminated")
156
+ return
157
+ }
158
+ }
159
+ }
160
+
161
+ // updateAllSessions 更新所有会话
162
+ func (su *SessionUpdater) updateAllSessions() {
163
+ log.Println("Starting session update for all sessions...")
164
+ // 复制当前会话列表,避免长时间持有锁
165
+ config.ConfigInstance.RwMutex.RLock()
166
+ sessionsCopy := make([]config.SessionInfo, len(config.ConfigInstance.Sessions))
167
+ copy(sessionsCopy, config.ConfigInstance.Sessions)
168
+ proxy := config.ConfigInstance.Proxy
169
+ config.ConfigInstance.RwMutex.RUnlock()
170
+ // 如果没有会话需要更新,直接返回
171
+ if len(sessionsCopy) == 0 {
172
+ log.Println("No sessions to update")
173
+ return
174
+ }
175
+ // 创建更新后的会话切片
176
+ updatedSessions := make([]config.SessionInfo, len(sessionsCopy))
177
+ var wg sync.WaitGroup
178
+ // 对每个会话执行更新
179
+ for i, session := range sessionsCopy {
180
+ wg.Add(1)
181
+ go func(index int, origSession config.SessionInfo) {
182
+ defer wg.Done()
183
+ // 创建客户端��更新 cookie
184
+ // 写死 model 和 openSearch 参数
185
+ client := core.NewClient(origSession.SessionKey, proxy, "claude-3-opus-20240229", false)
186
+ newCookie, err := client.GetNewCookie()
187
+ if err != nil {
188
+ log.Printf("Failed to update session %d: %v", index, err)
189
+ // 如果更新失败,保留原始会话
190
+ updatedSessions[index] = origSession
191
+ return
192
+ }
193
+ // 创建更新后的会话对象
194
+ updatedSessions[index] = config.SessionInfo{
195
+ SessionKey: newCookie,
196
+ }
197
+ }(i, session)
198
+ }
199
+ // 等待所有更新完成
200
+ wg.Wait()
201
+ // 一次性替换所有会话
202
+ config.ConfigInstance.RwMutex.Lock()
203
+ config.ConfigInstance.Sessions = updatedSessions
204
+ config.ConfigInstance.RwMutex.Unlock()
205
+ log.Printf("All %d sessions have been updated", len(updatedSessions))
206
+
207
+ // 保存更新后的配置到文件
208
+ if err := su.saveSessionsToFile(); err != nil {
209
+ log.Printf("Failed to save updated config: %v", err)
210
+ }
211
+ }
logger/logger.go ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package logger
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "time"
7
+
8
+ "github.com/fatih/color"
9
+ )
10
+
11
+ // 日志级别
12
+ const (
13
+ DEBUG = iota
14
+ INFO
15
+ WARN
16
+ ERROR
17
+ FATAL
18
+ )
19
+
20
+ var levelNames = map[int]string{
21
+ DEBUG: "DEBUG",
22
+ INFO: "INFO",
23
+ WARN: "WARN",
24
+ ERROR: "ERROR",
25
+ FATAL: "FATAL",
26
+ }
27
+
28
+ var levelColors = map[int]func(format string, a ...interface{}) string{
29
+ DEBUG: color.BlueString,
30
+ INFO: color.GreenString,
31
+ WARN: color.YellowString,
32
+ ERROR: color.RedString,
33
+ FATAL: color.New(color.FgHiRed, color.Bold).SprintfFunc(),
34
+ }
35
+
36
+ // 全局日志级别,默认为INFO
37
+ var logLevel = INFO
38
+
39
+ // SetLevel 设置日志级别
40
+ func SetLevel(level int) {
41
+ if level >= DEBUG && level <= FATAL {
42
+ logLevel = level
43
+ }
44
+ }
45
+
46
+ // GetLevel 获取当前日志级别
47
+ func GetLevel() int {
48
+ return logLevel
49
+ }
50
+
51
+ // GetLevelName 获取日志级别名称
52
+ func GetLevelName(level int) string {
53
+ if name, ok := levelNames[level]; ok {
54
+ return name
55
+ }
56
+ return "UNKNOWN"
57
+ }
58
+
59
+ // 基础日志打印函数
60
+ func log(level int, format string, args ...interface{}) {
61
+ if level < logLevel {
62
+ return
63
+ }
64
+
65
+ now := time.Now().Format("2006-01-02 15:04:05.000")
66
+ levelName := levelNames[level]
67
+ colorFunc := levelColors[level]
68
+
69
+ logContent := fmt.Sprintf(format, args...)
70
+ logPrefix := fmt.Sprintf("[%s] [%s] ", now, levelName)
71
+
72
+ // 使用颜色输出日志级别
73
+ fmt.Fprintf(os.Stdout, "%s%s\n", logPrefix, colorFunc(logContent))
74
+
75
+ // 如果是致命错误,则退出程序
76
+ if level == FATAL {
77
+ os.Exit(1)
78
+ }
79
+ }
80
+
81
+ // Debug 打印调试日志
82
+ func Debug(format string, args ...interface{}) {
83
+ log(DEBUG, format, args...)
84
+ }
85
+
86
+ // Info 打印信息日志
87
+ func Info(format string, args ...interface{}) {
88
+ log(INFO, format, args...)
89
+ }
90
+
91
+ // Warn 打印警告日志
92
+ func Warn(format string, args ...interface{}) {
93
+ log(WARN, format, args...)
94
+ }
95
+
96
+ // Error 打印错误日志
97
+ func Error(format string, args ...interface{}) {
98
+ log(ERROR, format, args...)
99
+ }
100
+
101
+ // Fatal 打印致命错误日志并退出程序
102
+ func Fatal(format string, args ...interface{}) {
103
+ log(FATAL, format, args...)
104
+ }
main.go ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "pplx2api/config"
5
+ "pplx2api/job"
6
+ "pplx2api/router"
7
+ "time"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func main() {
13
+ r := gin.Default()
14
+ // Load configuration
15
+
16
+ // Setup all routes
17
+ router.SetupRoutes(r)
18
+ // 创建会话更新器,设置更新间隔为24小时
19
+ sessionUpdater := job.GetSessionUpdater(24 * time.Hour)
20
+
21
+ // 启动会话更新器
22
+ sessionUpdater.Start()
23
+ defer sessionUpdater.Stop()
24
+
25
+ // Run the server on 0.0.0.0:8080
26
+ r.Run(config.ConfigInstance.Address)
27
+ }
middleware/auth.go ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middleware
2
+
3
+ import (
4
+ "pplx2api/config"
5
+ "strings"
6
+
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ // AuthMiddleware initializes the Claude client from the request header
11
+ func AuthMiddleware() gin.HandlerFunc {
12
+ return func(c *gin.Context) {
13
+ Key := c.GetHeader("Authorization")
14
+ if Key != "" {
15
+ Key = strings.TrimPrefix(Key, "Bearer ")
16
+ if Key != config.ConfigInstance.APIKey {
17
+ c.JSON(401, gin.H{
18
+ "error": "Invalid API key",
19
+ })
20
+ c.Abort()
21
+ return
22
+ }
23
+ c.Next()
24
+ return
25
+ }
26
+ c.JSON(401, gin.H{
27
+ "error": "Missing or invalid Authorization header",
28
+ })
29
+ c.Abort()
30
+ }
31
+ }
middleware/cors.go ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middleware
2
+
3
+ import "github.com/gin-gonic/gin"
4
+
5
+ // CORSMiddleware handles CORS headers
6
+ func CORSMiddleware() gin.HandlerFunc {
7
+ return func(c *gin.Context) {
8
+ c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
9
+ c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
10
+ c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, Authorization")
11
+ if c.Request.Method == "OPTIONS" {
12
+ c.AbortWithStatus(204)
13
+ return
14
+ }
15
+ c.Next()
16
+ }
17
+ }
model/openai.go ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package model
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "pplx2api/logger"
7
+ "time"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ "github.com/google/uuid"
11
+ )
12
+
13
+ type ChatCompletionRequest struct {
14
+ Model string `json:"model"`
15
+ Messages []map[string]interface{} `json:"messages"`
16
+ Stream bool `json:"stream"`
17
+ Tools []map[string]interface{} `json:"tools,omitempty"`
18
+ }
19
+
20
+ // OpenAISrteamResponse 定义 OpenAI 的流式响应结构
21
+ type OpenAISrteamResponse struct {
22
+ ID string `json:"id"`
23
+ Object string `json:"object"`
24
+ Created int64 `json:"created"`
25
+ Model string `json:"model"`
26
+ Choices []StreamChoice `json:"choices"`
27
+ }
28
+
29
+ // Choice 结构表示 OpenAI 返回的单个选项
30
+ type StreamChoice struct {
31
+ Index int `json:"index"`
32
+ Delta Delta `json:"delta"`
33
+ Logprobs interface{} `json:"logprobs"`
34
+ FinishReason interface{} `json:"finish_reason"`
35
+ }
36
+
37
+ type NoStreamChoice struct {
38
+ Index int `json:"index"`
39
+ Message Message `json:"message"`
40
+ Logprobs interface{} `json:"logprobs"`
41
+ FinishReason string `json:"finish_reason"`
42
+ }
43
+
44
+ // Delta 结构用于存储返回的文本内容
45
+ type Delta struct {
46
+ Content string `json:"content"`
47
+ }
48
+ type Message struct {
49
+ Role string `json:"role"`
50
+ Content string `json:"content"`
51
+ Refusal interface{} `json:"refusal"`
52
+ Annotation []interface{} `json:"annotation"`
53
+ }
54
+
55
+ type OpenAIResponse struct {
56
+ ID string `json:"id"`
57
+ Object string `json:"object"`
58
+ Created int64 `json:"created"`
59
+ Model string `json:"model"`
60
+ Choices []NoStreamChoice `json:"choices"`
61
+ Usage Usage `json:"usage"`
62
+ }
63
+ type Usage struct {
64
+ PromptTokens int `json:"prompt_tokens"`
65
+ CompletionTokens int `json:"completion_tokens"`
66
+ TotalTokens int `json:"total_tokens"`
67
+ }
68
+
69
+ func ReturnOpenAIResponse(text string, stream bool, gc *gin.Context) error {
70
+ if stream {
71
+ return streamRespose(text, gc)
72
+ } else {
73
+ return noStreamResponse(text, gc)
74
+ }
75
+ }
76
+
77
+ func streamRespose(text string, gc *gin.Context) error {
78
+ openAIResp := &OpenAISrteamResponse{
79
+ ID: uuid.New().String(),
80
+ Object: "chat.completion.chunk",
81
+ Created: time.Now().Unix(),
82
+ Model: "claude-3-7-sonnet-20250219",
83
+ Choices: []StreamChoice{
84
+ {
85
+ Index: 0,
86
+ Delta: Delta{
87
+ Content: text,
88
+ },
89
+ Logprobs: nil,
90
+ FinishReason: nil,
91
+ },
92
+ },
93
+ }
94
+
95
+ jsonBytes, err := json.Marshal(openAIResp)
96
+ jsonBytes = append([]byte("data: "), jsonBytes...)
97
+ jsonBytes = append(jsonBytes, []byte("\n\n")...)
98
+ if err != nil {
99
+ logger.Error(fmt.Sprintf("Error marshalling JSON: %v", err))
100
+ return err
101
+ }
102
+
103
+ // 发送数据
104
+ gc.Writer.Write(jsonBytes)
105
+ gc.Writer.Flush()
106
+ return nil
107
+ }
108
+
109
+ func noStreamResponse(text string, gc *gin.Context) error {
110
+ openAIResp := &OpenAIResponse{
111
+ ID: uuid.New().String(),
112
+ Object: "chat.completion",
113
+ Created: time.Now().Unix(),
114
+ Model: "claude-3-7-sonnet-20250219",
115
+ Choices: []NoStreamChoice{
116
+ {
117
+ Index: 0,
118
+ Message: Message{
119
+ Role: "assistant",
120
+ Content: text,
121
+ },
122
+ Logprobs: nil,
123
+ FinishReason: "stop",
124
+ },
125
+ },
126
+ }
127
+
128
+ gc.JSON(200, openAIResp)
129
+ return nil
130
+ }
router/router.go ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package router
2
+
3
+ import (
4
+ "pplx2api/middleware"
5
+ "pplx2api/service"
6
+
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ func SetupRoutes(r *gin.Engine) {
11
+ // Apply middleware
12
+ r.Use(middleware.CORSMiddleware())
13
+ r.Use(middleware.AuthMiddleware())
14
+
15
+ // Health check endpoint
16
+ r.GET("/health", service.HealthCheckHandler)
17
+
18
+ // Chat completions endpoint (OpenAI-compatible)
19
+ r.POST("/v1/chat/completions", service.ChatCompletionsHandler)
20
+ r.GET("/v1/models", service.MoudlesHandler)
21
+ // HuggingFace compatible routes
22
+ hfRouter := r.Group("/hf")
23
+ {
24
+ v1Router := hfRouter.Group("/v1")
25
+ {
26
+ v1Router.POST("/chat/completions", service.ChatCompletionsHandler)
27
+ v1Router.GET("/models", service.MoudlesHandler)
28
+ }
29
+ }
30
+ }
service/handle.go ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "fmt"
5
+ "net/http"
6
+ "pplx2api/config"
7
+ "pplx2api/core"
8
+ "pplx2api/logger"
9
+ "pplx2api/utils"
10
+ "strings"
11
+
12
+ "github.com/gin-gonic/gin"
13
+ )
14
+
15
+ type ChatCompletionRequest struct {
16
+ Model string `json:"model"`
17
+ Messages []map[string]interface{} `json:"messages"`
18
+ Stream bool `json:"stream"`
19
+ Tools []map[string]interface{} `json:"tools,omitempty"`
20
+ }
21
+
22
+ type ErrorResponse struct {
23
+ Error string `json:"error"`
24
+ }
25
+
26
+ // HealthCheckHandler handles the health check endpoint
27
+ func HealthCheckHandler(c *gin.Context) {
28
+ c.JSON(http.StatusOK, gin.H{
29
+ "status": "ok",
30
+ })
31
+ }
32
+
33
+ // ChatCompletionsHandler handles the chat completions endpoint
34
+ func ChatCompletionsHandler(c *gin.Context) {
35
+
36
+ // Parse request body
37
+ var req ChatCompletionRequest
38
+ if err := c.ShouldBindJSON(&req); err != nil {
39
+ c.JSON(http.StatusBadRequest, ErrorResponse{
40
+ Error: fmt.Sprintf("Invalid request: %v", err),
41
+ })
42
+ return
43
+ }
44
+ // logger.Info(fmt.Sprintf("Received request: %v", req))
45
+ // Validate request
46
+ if len(req.Messages) == 0 {
47
+ c.JSON(http.StatusBadRequest, ErrorResponse{
48
+ Error: "No messages provided",
49
+ })
50
+ return
51
+ }
52
+
53
+ // Get model or use default
54
+ model := req.Model
55
+ if model == "" {
56
+ model = "claude-3.7-sonnet"
57
+ }
58
+ openSearch := false
59
+ if strings.HasSuffix(model, "-search") {
60
+ openSearch = true
61
+ model = strings.TrimSuffix(model, "-search")
62
+ }
63
+ model = config.ModelMapGet(model, model) // 获取模型名称
64
+ var prompt strings.Builder
65
+ img_data_list := []string{}
66
+ // Format messages into a single prompt
67
+ for _, msg := range req.Messages {
68
+ role, roleOk := msg["role"].(string)
69
+ if !roleOk {
70
+ continue // 忽略无效格式
71
+ }
72
+
73
+ content, exists := msg["content"]
74
+ if !exists {
75
+ continue
76
+ }
77
+
78
+ prompt.WriteString(utils.GetRolePrefix(role)) // 获取角色前缀
79
+ switch v := content.(type) {
80
+ case string: // 如果 content 直接是 string
81
+ prompt.WriteString(v + "\n\n")
82
+ case []interface{}: // 如果 content 是 []interface{} 类型的数组
83
+ for _, item := range v {
84
+ if itemMap, ok := item.(map[string]interface{}); ok {
85
+ if itemType, ok := itemMap["type"].(string); ok {
86
+ if itemType == "text" {
87
+ if text, ok := itemMap["text"].(string); ok {
88
+ prompt.WriteString(text + "\n\n")
89
+ }
90
+ } else if itemType == "image_url" {
91
+ if imageUrl, ok := itemMap["image_url"].(map[string]interface{}); ok {
92
+ if url, ok := imageUrl["url"].(string); ok {
93
+ if len(url) > 50 {
94
+ logger.Info(fmt.Sprintf("Image URL: %s ……", url[:50]))
95
+ }
96
+ if strings.HasPrefix(url, "data:image/") {
97
+ // 保留 base64 编码的图片数据
98
+ url = strings.Split(url, ",")[1]
99
+ }
100
+ img_data_list = append(img_data_list, url) // 收集图片数据
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ fmt.Println(prompt.String()) // 输出最终构造的内容
110
+ fmt.Println("img_data_list_length:", len(img_data_list)) // 输出图片数据列表长度
111
+ var rootPrompt strings.Builder
112
+ rootPrompt.WriteString(prompt.String())
113
+ // 切号重试机制
114
+ var pplxClient *core.Client
115
+ index := config.Sr.NextIndex()
116
+ for i := 0; i < config.ConfigInstance.RetryCount; i++ {
117
+ if i > 0 {
118
+ prompt.Reset()
119
+ prompt.WriteString(rootPrompt.String())
120
+ }
121
+ index = (index + 1) % len(config.ConfigInstance.Sessions)
122
+ session, err := config.ConfigInstance.GetSessionForModel(index)
123
+ logger.Info(fmt.Sprintf("Using session for model %s: %s", model, session.SessionKey))
124
+ if err != nil {
125
+ logger.Error(fmt.Sprintf("Failed to get session for model %s: %v", model, err))
126
+ logger.Info("Retrying another session")
127
+ continue
128
+ }
129
+ // Initialize the Claude client
130
+ pplxClient = core.NewClient(session.SessionKey, config.ConfigInstance.Proxy, model, openSearch)
131
+ if len(img_data_list) > 0 {
132
+ err := pplxClient.UploadImage(img_data_list)
133
+ if err != nil {
134
+ logger.Error(fmt.Sprintf("Failed to upload file: %v", err))
135
+ logger.Info("Retrying another session")
136
+
137
+ continue
138
+ }
139
+ }
140
+ if prompt.Len() > config.ConfigInstance.MaxChatHistoryLength {
141
+ err := pplxClient.UploadText(prompt.String())
142
+ if err != nil {
143
+ logger.Error(fmt.Sprintf("Failed to upload text: %v", err))
144
+ logger.Info("Retrying another session")
145
+
146
+ continue
147
+ }
148
+ prompt.Reset()
149
+ prompt.WriteString(config.ConfigInstance.PromptForFile)
150
+ }
151
+ if _, err := pplxClient.SendMessage(prompt.String(), req.Stream, config.ConfigInstance.IsIncognito, c); err != nil {
152
+ logger.Error(fmt.Sprintf("Failed to send message: %v", err))
153
+ logger.Info("Retrying another session")
154
+
155
+ continue // Retry on error
156
+ }
157
+
158
+ return
159
+
160
+ }
161
+ logger.Error("Failed for all retries")
162
+ c.JSON(http.StatusInternalServerError, ErrorResponse{
163
+ Error: "Failed to process request after multiple attempts"})
164
+ }
165
+
166
+ func MoudlesHandler(c *gin.Context) {
167
+ c.JSON(http.StatusOK, gin.H{
168
+ "data": config.ResponseModles,
169
+ })
170
+ }
utils/imageShow.go ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ package utils
2
+
3
+ import "fmt"
4
+
5
+ func ImageShow(index int, modelName, url string) string {
6
+ index++
7
+ return fmt.Sprintf("![%s](%s)", modelName, url)
8
+ }
utils/random.go ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package utils
2
+
3
+ import "math/rand"
4
+
5
+ func RandomString(length int) string {
6
+ const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
7
+ result := make([]byte, length)
8
+ for i := range result {
9
+ result[i] = charset[rand.Intn(len(charset))]
10
+ }
11
+ return string(result)
12
+ }
utils/role.go ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package utils
2
+
3
+ import (
4
+ "pplx2api/config"
5
+ )
6
+
7
+ // **获取角色前缀**
8
+ func GetRolePrefix(role string) string {
9
+ if config.ConfigInstance.NoRolePrefix {
10
+ return ""
11
+ }
12
+ switch role {
13
+ case "system":
14
+ return "System: "
15
+ case "user":
16
+ return "Human: "
17
+ case "assistant":
18
+ return "Assistant: "
19
+ default:
20
+ return "Unknown: "
21
+ }
22
+ }
utils/searchShow.go ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package utils
2
+
3
+ import (
4
+ "fmt"
5
+ "pplx2api/config"
6
+ )
7
+
8
+ func searchShowDetails(index int, title, url, snippet string) string {
9
+ return fmt.Sprintf("<details>\n<summary>[%d] %s</summary>\n\n%s\n\n[Link](%s)\n\n</details>", index, title, snippet, url)
10
+ }
11
+
12
+ func searchShowCompatible(index int, title, url, snippet string) string {
13
+ return fmt.Sprintf("[%d] [%s](%s):\n%s\n", index, title, url, snippet)
14
+ }
15
+
16
+ func SearchShow(index int, title, url, snippet string) string {
17
+ index++
18
+ if len([]rune(snippet)) > 150 {
19
+ runeSnippet := []rune(snippet)
20
+ snippet = fmt.Sprintf("%s ……", string(runeSnippet[:150]))
21
+ }
22
+ if config.ConfigInstance.SearchResultCompatible {
23
+ return searchShowCompatible(index, title, url, snippet)
24
+ }
25
+ return searchShowDetails(index, title, url, snippet)
26
+ }