Files changed (4) hide show
  1. Dockerfile +39 -0
  2. README.md +11 -11
  3. api-pool.go +488 -0
  4. entrypoint.sh +56 -0
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- 第一阶段:构建阶段 (Builder Stage) ---
2
+ # 使用官方 Go 镜像进行编译
3
+ FROM golang:1.22-alpine AS builder
4
+
5
+ # 设置工作目录
6
+ WORKDIR /build
7
+
8
+ # 复制 Go 源代码文件
9
+ COPY api-pool.go .
10
+
11
+ # 编译 Go 应用
12
+ # CGO_ENABLED=0 尝试静态链接,减少依赖
13
+ # -ldflags="-w -s" 减小二进制文件大小
14
+ # -o /app/api-pool 指定输出路径和名称
15
+ RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /app/api-pool api-pool.go
16
+
17
+ # --- 第二阶段:运行阶段 (Final Stage) ---
18
+ # 使用轻量的 Alpine 镜像作为最终运行环境
19
+ FROM alpine:latest
20
+
21
+ # 设置工作目录
22
+ WORKDIR /app
23
+
24
+ # 从构建阶段复制编译好的二进制文件
25
+ COPY --from=builder /app/api-pool /app/api-pool
26
+
27
+ # 复制启动脚本
28
+ COPY entrypoint.sh /app/entrypoint.sh
29
+
30
+ # 赋予执行权限
31
+ RUN chmod +x /app/api-pool /app/entrypoint.sh
32
+
33
+ # 暴露应用程序监听的端口 (根据您的参数是 6969)
34
+ EXPOSE 6969
35
+
36
+ # 设置容器的入口点为启动脚本
37
+ ENTRYPOINT ["/app/entrypoint.sh"]
38
+
39
+ # 注意:CMD 指令现在由 entrypoint.sh 脚本通过 exec 来执行
README.md CHANGED
@@ -1,11 +1,11 @@
1
- ---
2
- title: Api Pool
3
- emoji: 🏃
4
- colorFrom: pink
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: gpl-3.0
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: API Key Pool # 您可以修改标题
3
+ emoji: 🔑 # 您可以修改 Emoji
4
+ colorFrom: green # 您可以修改颜色
5
+ colorTo: blue # 您可以修改颜色
6
+ sdk: docker
7
+ app_port: 6969 # 必须与您的 --port 参数和 EXPOSE 端口一致
8
+ pinned: false
9
+ ---
10
+
11
+ (在此添加您的 Space 描述)
api-pool.go ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "encoding/json"
6
+ "flag"
7
+ "io"
8
+ "log"
9
+ "net/http"
10
+ "net/url"
11
+ "os"
12
+ "path"
13
+ "strings"
14
+ "sync"
15
+ )
16
+
17
+ // Config 结构体用于存储命令行参数配置
18
+ type Config struct {
19
+ KeyFile string // API 密钥文件路径
20
+ TargetURL string // 目标 API 基础 URL
21
+ Port string // 代理服务器监听端口
22
+ Address string // 代理服务器监听地址
23
+ Password string // 客户端身份验证密码
24
+ MaxWorkers int // 最大工作协程数
25
+ MaxQueue int // 最大请求队列长度
26
+ }
27
+
28
+ // parseFlags 解析命令行参数并返回 Config 实例
29
+ func parseFlags() *Config {
30
+ cfg := &Config{}
31
+ flag.StringVar(&cfg.KeyFile, "key-file", "", "Path to the API key file")
32
+ flag.StringVar(&cfg.TargetURL, "target-url", "", "Target API base URL")
33
+ flag.StringVar(&cfg.Port, "port", "8080", "Port to listen on")
34
+ flag.StringVar(&cfg.Address, "address", "localhost", "Address to listen on")
35
+ flag.StringVar(&cfg.Password, "password", "", "Password for client authentication")
36
+
37
+ // 添加WorkerPool相关配置
38
+ maxWorkers := flag.Int("max-workers", 50, "Maximum number of worker goroutines")
39
+ maxQueue := flag.Int("max-queue", 500, "Maximum size of request queue")
40
+
41
+ flag.Parse()
42
+
43
+ // 将WorkerPool配置添加到Config结构体
44
+ cfg.MaxWorkers = *maxWorkers
45
+ cfg.MaxQueue = *maxQueue
46
+
47
+ return cfg
48
+ }
49
+
50
+ // KeyPool 管理 API 密钥池
51
+ type KeyPool struct {
52
+ keys []string // 密钥列表
53
+ mu sync.Mutex // 互斥锁,确保线程安全
54
+ currentIndex int // 当前密钥索引,用于循环抽取
55
+ }
56
+
57
+ // NewKeyPool 从文件中加载密钥并创建 KeyPool 实例
58
+ func NewKeyPool(filePath string) (*KeyPool, error) {
59
+ file, err := os.Open(filePath)
60
+ if err != nil {
61
+ log.Printf("[ERROR] Failed to open key file %s: %v", filePath, err)
62
+ return nil, err
63
+ }
64
+ defer file.Close()
65
+
66
+ var keys []string
67
+ scanner := bufio.NewScanner(file)
68
+ for scanner.Scan() {
69
+ key := strings.TrimSpace(scanner.Text())
70
+ if key != "" {
71
+ keys = append(keys, key)
72
+ }
73
+ }
74
+ if err := scanner.Err(); err != nil {
75
+ log.Printf("[ERROR] Failed to read key file %s: %v", filePath, err)
76
+ return nil, err
77
+ }
78
+ log.Printf("[INFO] Loaded %d keys from file %s", len(keys), filePath)
79
+ return &KeyPool{keys: keys, currentIndex: 0}, nil
80
+ }
81
+
82
+ // GetRandomKey 按顺序循环返回一个密钥
83
+ func (kp *KeyPool) GetRandomKey() string {
84
+ kp.mu.Lock()
85
+ defer kp.mu.Unlock()
86
+ if len(kp.keys) == 0 {
87
+ return ""
88
+ }
89
+ key := kp.keys[kp.currentIndex]
90
+ kp.currentIndex = (kp.currentIndex + 1) % len(kp.keys) // 循环到下一个索引
91
+ return key
92
+ }
93
+
94
+ // 定义请求结构体
95
+ type ProxyRequest struct {
96
+ Request *http.Request
97
+ Response http.ResponseWriter
98
+ Done chan bool // 用于通知请求处理完成
99
+ }
100
+
101
+ // Worker结构体,表示一个工作协程
102
+ type Worker struct {
103
+ ID int
104
+ TaskQueue chan *ProxyRequest // 任务队列
105
+ Quit chan bool // 退出信号
106
+ WorkerPool *WorkerPool // 所属工作池
107
+ }
108
+
109
+ // 创建新的Worker
110
+ func NewWorker(id int, workerPool *WorkerPool) *Worker {
111
+ return &Worker{
112
+ ID: id,
113
+ TaskQueue: make(chan *ProxyRequest),
114
+ Quit: make(chan bool),
115
+ WorkerPool: workerPool,
116
+ }
117
+ }
118
+
119
+ // Worker开始工作
120
+ func (w *Worker) Start() {
121
+ go func() {
122
+ for {
123
+ // 将worker注册到工作池的空闲队列
124
+ w.WorkerPool.WorkerQueue <- w.TaskQueue
125
+
126
+ select {
127
+ case task := <-w.TaskQueue:
128
+ // 处理请求
129
+ w.WorkerPool.HandleFunc(task.Response, task.Request)
130
+ task.Done <- true
131
+ case <-w.Quit:
132
+ // 收到退出信号
133
+ return
134
+ }
135
+ }
136
+ }()
137
+ }
138
+
139
+ // Worker停止工作
140
+ func (w *Worker) Stop() {
141
+ go func() {
142
+ w.Quit <- true
143
+ }()
144
+ }
145
+
146
+ // WorkerPool结构体,管理工作协程池
147
+ type WorkerPool struct {
148
+ WorkerQueue chan chan *ProxyRequest // 空闲Worker队列
149
+ TaskQueue chan *ProxyRequest // 任务队列
150
+ MaxWorkers int // 最大Worker数量
151
+ MaxQueue int // 最大队列长度
152
+ HandleFunc func(http.ResponseWriter, *http.Request) // 请求处理函数
153
+ }
154
+
155
+ // 创建新的WorkerPool
156
+ func NewWorkerPool(maxWorkers int, maxQueue int, handleFunc func(http.ResponseWriter, *http.Request)) *WorkerPool {
157
+ pool := &WorkerPool{
158
+ WorkerQueue: make(chan chan *ProxyRequest, maxWorkers),
159
+ TaskQueue: make(chan *ProxyRequest, maxQueue),
160
+ MaxWorkers: maxWorkers,
161
+ MaxQueue: maxQueue,
162
+ HandleFunc: handleFunc,
163
+ }
164
+ return pool
165
+ }
166
+
167
+ // 启动WorkerPool
168
+ func (wp *WorkerPool) Start() {
169
+ // 创建并启动workers
170
+ for i := 0; i < wp.MaxWorkers; i++ {
171
+ worker := NewWorker(i, wp)
172
+ worker.Start()
173
+ log.Printf("[INFO] Started worker %d", i)
174
+ }
175
+
176
+ // 启动任务分发协程
177
+ go wp.dispatch()
178
+ }
179
+
180
+ // 停止WorkerPool
181
+ func (wp *WorkerPool) Stop() {
182
+ // TODO: 实现停止逻辑
183
+ }
184
+
185
+ // 将任务分发给空闲worker
186
+ func (wp *WorkerPool) dispatch() {
187
+ for {
188
+ select {
189
+ case task := <-wp.TaskQueue:
190
+ // 等待空闲worker
191
+ workerTaskQueue := <-wp.WorkerQueue
192
+ // 将任务发送给worker
193
+ workerTaskQueue <- task
194
+ }
195
+ }
196
+ }
197
+
198
+ // 将请求提交到WorkerPool
199
+ func (wp *WorkerPool) Submit(response http.ResponseWriter, request *http.Request) bool {
200
+ task := &ProxyRequest{
201
+ Request: request,
202
+ Response: response,
203
+ Done: make(chan bool, 1),
204
+ }
205
+
206
+ select {
207
+ case wp.TaskQueue <- task:
208
+ // 请求成功加入队列
209
+ <-task.Done // 等待任务完成
210
+ return true
211
+ default:
212
+ // 队列已满,实现背压
213
+ log.Println("[WARN] Task queue is full, rejecting request")
214
+ http.Error(response, "Server is busy, please try again later", http.StatusServiceUnavailable)
215
+ return false
216
+ }
217
+ }
218
+
219
+ // ProxyHandler 处理 HTTP 代理请求
220
+ type ProxyHandler struct {
221
+ cfg *Config // 配置信息
222
+ keyPool *KeyPool // 密钥池
223
+ client *http.Client // HTTP 客户端
224
+ workerPool *WorkerPool // 工作协程池
225
+ }
226
+
227
+ // NewProxyHandler 创建 ProxyHandler 实例
228
+ func NewProxyHandler(cfg *Config, keyPool *KeyPool) *ProxyHandler {
229
+ handler := &ProxyHandler{
230
+ cfg: cfg,
231
+ keyPool: keyPool,
232
+ client: &http.Client{},
233
+ }
234
+ return handler
235
+ }
236
+
237
+ // InitWorkerPool 初始化工作协程池
238
+ func (ph *ProxyHandler) InitWorkerPool(maxWorkers int, maxQueue int) {
239
+ ph.workerPool = NewWorkerPool(maxWorkers, maxQueue, ph.HandleRequest)
240
+ ph.workerPool.Start()
241
+ log.Printf("[INFO] Started worker pool with %d workers and queue size %d", maxWorkers, maxQueue)
242
+ }
243
+
244
+ // ServeHTTP 实现 HTTP 处理逻辑
245
+ func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
246
+ // 记录接收到的请求
247
+ log.Printf("[INFO] Received request: %s %s", r.Method, r.URL.String())
248
+
249
+ // 将请求提交到工作池处理
250
+ ph.workerPool.Submit(w, r)
251
+ }
252
+
253
+ // HandleRequest 处理请求的方法,由Worker调用
254
+ func (ph *ProxyHandler) HandleRequest(w http.ResponseWriter, r *http.Request) {
255
+ // 验证客户端身份
256
+ if !ph.authenticate(r) {
257
+ log.Println("[WARN] Unauthorized access attempt")
258
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
259
+ return
260
+ }
261
+ log.Println("[INFO] Authentication successful")
262
+
263
+ // 尝试解析请求体中的模型信息
264
+ model, err := ph.extractModelFromRequest(r)
265
+ if err != nil {
266
+ log.Printf("[WARN] Failed to extract model from request: %v", err)
267
+ } else if model != "" {
268
+ log.Printf("[INFO] Model specified in request: %s", model)
269
+ }
270
+
271
+ // 构建目标 URL
272
+ targetURL, err := ph.buildTargetURL(r)
273
+ if err != nil {
274
+ log.Printf("[ERROR] Failed to build target URL: %v", err)
275
+ http.Error(w, "Bad Request", http.StatusBadRequest)
276
+ return
277
+ }
278
+ log.Printf("[INFO] Target URL: %s", targetURL)
279
+
280
+ // 重试逻辑
281
+ maxRetries := len(ph.keyPool.keys)
282
+ attemptedKeys := make(map[string]bool)
283
+ log.Printf("[INFO] Starting key selection process, total keys available: %d", maxRetries)
284
+
285
+ for i := 0; i < maxRetries; i++ {
286
+ key := ph.getUnusedKey(attemptedKeys)
287
+ if key == "" {
288
+ log.Printf("[ERROR] No unused keys remaining after %d attempts", i)
289
+ break
290
+ }
291
+ attemptedKeys[key] = true
292
+ maskedKey := maskKey(key)
293
+ log.Printf("[INFO] Attempt %d/%d: Selecting key %s", i+1, maxRetries, maskedKey)
294
+
295
+ // 创建请求
296
+ req, err := ph.createRequest(r, targetURL, key)
297
+ if err != nil {
298
+ log.Printf("[ERROR] Failed to create request with key %s: %v", maskedKey, err)
299
+ log.Printf("[INFO] Switching to another key due to request creation failure")
300
+ continue
301
+ }
302
+
303
+ // 发送请求
304
+ log.Printf("[INFO] Sending request to target API with key %s", maskedKey)
305
+ resp, err := ph.client.Do(req)
306
+ if err != nil {
307
+ log.Printf("[ERROR] Failed to send request with key %s: %v", maskedKey, err)
308
+ log.Printf("[INFO] Switching to another key due to network error")
309
+ continue
310
+ }
311
+ defer resp.Body.Close()
312
+
313
+ // 处理响应
314
+ log.Printf("[INFO] Received response with status code %d", resp.StatusCode)
315
+ if resp.StatusCode >= 200 && resp.StatusCode < 300 {
316
+ log.Println("[INFO] Request successful, forwarding response")
317
+ ph.forwardResponse(w, resp)
318
+ return
319
+ } else if resp.StatusCode == 403 || resp.StatusCode == 429 {
320
+ log.Printf("[WARN] Received %d status code with key %s", resp.StatusCode, maskedKey)
321
+ log.Printf("[INFO] Switching to another key due to status code %d", resp.StatusCode)
322
+ continue
323
+ } else {
324
+ log.Printf("[INFO] Forwarding response with status code %d", resp.StatusCode)
325
+ ph.forwardResponse(w, resp)
326
+ return
327
+ }
328
+ }
329
+
330
+ // 所有密钥尝试后仍失败
331
+ log.Printf("[ERROR] All %d keys failed after retries", maxRetries)
332
+ http.Error(w, "Failed to get response from API after all retries", http.StatusBadGateway)
333
+ }
334
+
335
+ // getUnusedKey 获取一个未使用过的密钥
336
+ func (ph *ProxyHandler) getUnusedKey(attempted map[string]bool) string {
337
+ key := ph.keyPool.GetRandomKey()
338
+ // 如果获取到的密钥已使用过,则尝试其他密钥
339
+ for attempted[key] && len(attempted) < len(ph.keyPool.keys) {
340
+ key = ph.keyPool.GetRandomKey()
341
+ }
342
+ // 如果所有密钥都已尝试过,返回空字符串
343
+ if attempted[key] {
344
+ return ""
345
+ }
346
+ return key
347
+ }
348
+
349
+ // authenticate 验证客户端身份
350
+ func (ph *ProxyHandler) authenticate(r *http.Request) bool {
351
+ authHeader := r.Header.Get("Authorization")
352
+ if authHeader == "" {
353
+ return false
354
+ }
355
+ parts := strings.Split(authHeader, " ")
356
+ if len(parts) != 2 || parts[0] != "Bearer" {
357
+ return false
358
+ }
359
+ return parts[1] == ph.cfg.Password
360
+ }
361
+
362
+ // buildTargetURL 构建目标 API 的完整 URL
363
+ func (ph *ProxyHandler) buildTargetURL(r *http.Request) (string, error) {
364
+ u, err := url.Parse(ph.cfg.TargetURL)
365
+ if err != nil {
366
+ return "", err
367
+ }
368
+ u.Path = path.Join(u.Path, r.URL.Path)
369
+ u.RawQuery = r.URL.RawQuery
370
+ return u.String(), nil
371
+ }
372
+
373
+ // createRequest 创建转发请求
374
+ func (ph *ProxyHandler) createRequest(r *http.Request, targetURL, key string) (*http.Request, error) {
375
+ req, err := http.NewRequest(r.Method, targetURL, r.Body)
376
+ if err != nil {
377
+ return nil, err
378
+ }
379
+
380
+ // 复制并修改请求头
381
+ for k, v := range r.Header {
382
+ if k != "Host" && k != "Connection" && k != "Proxy-Connection" && k != "Authorization" {
383
+ req.Header[k] = v
384
+ }
385
+ }
386
+ req.Header.Set("Authorization", "Bearer "+key)
387
+ return req, nil
388
+ }
389
+
390
+ // forwardResponse 将响应转发给客户端,支持流式和非流式
391
+ func (ph *ProxyHandler) forwardResponse(w http.ResponseWriter, resp *http.Response) {
392
+ // 设置响应头
393
+ for k, v := range resp.Header {
394
+ w.Header()[k] = v
395
+ }
396
+ w.WriteHeader(resp.StatusCode)
397
+
398
+ // 处理流式响应
399
+ if strings.Contains(resp.Header.Get("Content-Type"), "text/event-stream") || resp.Header.Get("Transfer-Encoding") == "chunked" {
400
+ log.Println("[INFO] Handling streaming response")
401
+ flusher, ok := w.(http.Flusher)
402
+ if !ok {
403
+ log.Println("[ERROR] Streaming unsupported by server")
404
+ http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
405
+ return
406
+ }
407
+ reader := bufio.NewReader(resp.Body)
408
+ for {
409
+ line, err := reader.ReadBytes('\n')
410
+ if err != nil {
411
+ if err == io.EOF {
412
+ log.Println("[INFO] Stream ended")
413
+ break
414
+ }
415
+ log.Printf("[ERROR] Error reading stream: %v", err)
416
+ http.Error(w, "Error reading stream", http.StatusInternalServerError)
417
+ return
418
+ }
419
+ w.Write(line)
420
+ flusher.Flush()
421
+ }
422
+ } else {
423
+ // 非流式响应,直接复制
424
+ _, err := io.Copy(w, resp.Body)
425
+ if err != nil {
426
+ log.Printf("[ERROR] Failed to forward response: %v", err)
427
+ }
428
+ }
429
+ }
430
+
431
+ // extractModelFromRequest 尝试从请求体中提取模型名称
432
+ func (ph *ProxyHandler) extractModelFromRequest(r *http.Request) (string, error) {
433
+ if r.Body == nil {
434
+ return "", nil
435
+ }
436
+ body, err := io.ReadAll(r.Body)
437
+ if err != nil {
438
+ return "", err
439
+ }
440
+ r.Body = io.NopCloser(strings.NewReader(string(body)))
441
+
442
+ var data map[string]interface{}
443
+ if err := json.Unmarshal(body, &data); err != nil {
444
+ return "", err
445
+ }
446
+ if model, ok := data["model"].(string); ok {
447
+ return model, nil
448
+ }
449
+ return "", nil
450
+ }
451
+
452
+ // maskKey 直接返回原始密钥,不再进行掩码处理
453
+ func maskKey(key string) string {
454
+ return key
455
+ }
456
+
457
+ // main 函数,启动代理服务器
458
+ func main() {
459
+ // 解析配置
460
+ cfg := parseFlags()
461
+ if cfg.KeyFile == "" || cfg.TargetURL == "" || cfg.Password == "" {
462
+ log.Println("[ERROR] Missing required flags: --key-file, --target-url, --password")
463
+ os.Exit(1)
464
+ }
465
+ log.Printf("[INFO] Configuration loaded: KeyFile=%s, TargetURL=%s, Address=%s, Port=%s, MaxWorkers=%d, MaxQueue=%d",
466
+ cfg.KeyFile, cfg.TargetURL, cfg.Address, cfg.Port, cfg.MaxWorkers, cfg.MaxQueue)
467
+
468
+ // 初始化密钥池
469
+ keyPool, err := NewKeyPool(cfg.KeyFile)
470
+ if err != nil {
471
+ log.Printf("[ERROR] Failed to initialize key pool: %v", err)
472
+ os.Exit(1)
473
+ }
474
+
475
+ // 创建代理处理器
476
+ proxyHandler := NewProxyHandler(cfg, keyPool)
477
+
478
+ // 初始化并启动工作池
479
+ proxyHandler.InitWorkerPool(cfg.MaxWorkers, cfg.MaxQueue)
480
+
481
+ // 启动服务器
482
+ addr := cfg.Address + ":" + cfg.Port
483
+ log.Printf("[INFO] Starting proxy server on %s", addr)
484
+ if err := http.ListenAndServe(addr, proxyHandler); err != nil {
485
+ log.Printf("[ERROR] Failed to start server: %v", err)
486
+ os.Exit(1)
487
+ }
488
+ }
entrypoint.sh ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ # 如果命令失败则立即退出
3
+ set -e
4
+
5
+ # 定义临时密钥文件的路径
6
+ KEY_FILE_PATH="/tmp/keys.txt"
7
+
8
+ echo "--- 正在检查 Secrets ---"
9
+
10
+ # 检查 API_PASSWORD 是否已设置
11
+ if [ -z "${API_PASSWORD}" ]; then
12
+ echo "[错误] 必须在 Hugging Face Secrets 中设置 API_PASSWORD !"
13
+ exit 1
14
+ fi
15
+
16
+ # 检查 key_list 是否已设置
17
+ if [ -z "${key_list}" ]; then
18
+ echo "[错误] 必须在 Hugging Face Secrets 中设置 key_list !"
19
+ exit 1
20
+ fi
21
+
22
+ echo "--- 正在从 Secret 'key_list' 创建临时密钥文件 (${KEY_FILE_PATH}) ---"
23
+
24
+ # 从环境变量 key_list 读取内容,并写入临时文件
25
+ # 使用 'echo -e' 来解释可能存在的 '\n' 换行符
26
+ # 将标准错误重定向到 /dev/null 以避免打印潜在的密钥内容(尽管通常 echo 不会)
27
+ echo -e "${key_list}" > "${KEY_FILE_PATH}" 2>/dev/null
28
+
29
+ # 验证文件是否创建成功且非空
30
+ if [ ! -s "${KEY_FILE_PATH}" ]; then
31
+ echo "[错误] 创建密钥文件失败或文件为空!请检查 'key_list' Secret 的内容。"
32
+ exit 1
33
+ fi
34
+
35
+ echo "--- 密钥文件已生成 ---"
36
+
37
+ # !!! 【重要】生产环境中不要取消下面这行的注释,避免日志泄露 !!!
38
+ # echo "密钥文件内容预览 (前几行):"
39
+ # head -n 3 "${KEY_FILE_PATH}"
40
+
41
+ echo "--- 正在启动 api-pool 服务 ---"
42
+
43
+ # 使用 exec 执行 Go 程序,将脚本进程替换为 Go 程序进程
44
+ # 将临时文件路径传给 --key-file
45
+ # 将从 Secret 读取的密码传给 --password
46
+ # 将地址设为 0.0.0.0 以便容器外访问
47
+ # 传入其他您指定的参数
48
+ exec /app/api-pool \
49
+ --key-file "${KEY_FILE_PATH}" \
50
+ --target-url "https://api.siliconflow.cn" \
51
+ --port "6969" \
52
+ --address "0.0.0.0" \
53
+ --password "${API_PASSWORD}" \
54
+ --max-workers=1000 \
55
+ --max-queue=2000
56
+ # 注意:--max-workers 和 --max-queue 值较高,请关注 Space 资源使用情况