CoolFace
Apppublic

drriver/gemini-rproxy2

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
app.py707 linesDownload Raw Back to root
1from flask import Flask, request, jsonify, Response, stream_with_context, render_template_string
2import json
3import os
4import re
5import logging
6import func
7from datetime import datetime, timedelta
8from apscheduler.schedulers.background import BackgroundScheduler
9import time
10import requests
11from collections import deque
12import random
13from dataclasses import dataclass
14from typing import Optional, Dict, Any
15
16app = Flask(__name__)
17
18os.environ['TZ'] = 'Asia/Shanghai'
19
20app = Flask(__name__)
21
22app.secret_key = os.urandom(24)
23
24formatter = logging.Formatter('%(message)s')
25logger = logging.getLogger(__name__)
26logger.setLevel(logging.INFO)
27handler = logging.StreamHandler()
28handler.setFormatter(formatter)
29logger.addHandler(handler)
30
31MAX_RETRIES = int(os.environ.get('MaxRetries', '3').strip() or '3')
32MAX_REQUESTS = int(os.environ.get('MaxRequests', '2').strip() or '2')
33LIMIT_WINDOW = int(os.environ.get('LimitWindow', '60').strip() or '60')
34
35RETRY_DELAY = 1
36MAX_RETRY_DELAY = 16
37
38request_counts = {}
39
40api_key_blacklist = set()
41api_key_blacklist_duration = 60
42
43
44safety_settings = [
45    {
46        "category": "HARM_CATEGORY_HARASSMENT",
47        "threshold": "BLOCK_NONE"
48    },
49    {
50        "category": "HARM_CATEGORY_HATE_SPEECH",
51        "threshold": "BLOCK_NONE"
52    },
53    {
54        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
55        "threshold": "BLOCK_NONE"
56    },
57    {
58        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
59        "threshold": "BLOCK_NONE"
60    },
61    {
62        "category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
63        "threshold": 'BLOCK_NONE'
64    }
65]
66safety_settings_g2 = [
67    {
68        "category": "HARM_CATEGORY_HARASSMENT",
69        "threshold": "OFF"
70    },
71    {
72        "category": "HARM_CATEGORY_HATE_SPEECH",
73        "threshold": "OFF"
74    },
75    {
76        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
77        "threshold": "OFF"
78    },
79    {
80        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
81        "threshold": "OFF"
82    },
83    {
84        "category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
85        "threshold": 'OFF'
86    }
87]
88@dataclass
89class GeneratedText:
90    text: str
91    finish_reason: Optional[str] = None
92
93
94class ResponseWrapper:
95    def __init__(self, data: Dict[Any, Any]):
96        self._data = data
97        self._text = self._extract_text()
98        self._finish_reason = self._extract_finish_reason()
99        self._prompt_token_count = self._extract_prompt_token_count()
100        self._candidates_token_count = self._extract_candidates_token_count()
101        self._total_token_count = self._extract_total_token_count()
102        self._thoughts = self._extract_thoughts()
103        self._json_dumps = json.dumps(self._data, indent=4, ensure_ascii=False)
104
105    def _extract_thoughts(self) -> Optional[str]:
106        try:
107            for part in self._data['candidates'][0]['content']['parts']:
108                if 'thought' in part:
109                    return part['text']
110            return ""
111        except (KeyError, IndexError):
112            return ""
113
114    def _extract_text(self) -> str:
115        try:
116            for part in self._data['candidates'][0]['content']['parts']:
117                if 'thought' not in part:
118                    return part['text']
119            return ""
120        except (KeyError, IndexError):
121            return ""
122
123    def _extract_finish_reason(self) -> Optional[str]:
124        try:
125            return self._data['candidates'][0].get('finishReason')
126        except (KeyError, IndexError):
127            return None
128
129    def _extract_prompt_token_count(self) -> Optional[int]:
130        try:
131            return self._data['usageMetadata'].get('promptTokenCount')
132        except (KeyError):
133            return None
134
135    def _extract_candidates_token_count(self) -> Optional[int]:
136        try:
137            return self._data['usageMetadata'].get('candidatesTokenCount')
138        except (KeyError):
139            return None
140
141    def _extract_total_token_count(self) -> Optional[int]:
142        try:
143            return self._data['usageMetadata'].get('totalTokenCount')
144        except (KeyError):
145            return None
146
147    @property
148    def text(self) -> str:
149        return self._text
150
151    @property
152    def finish_reason(self) -> Optional[str]:
153        return self._finish_reason
154
155    @property
156    def prompt_token_count(self) -> Optional[int]:
157        return self._prompt_token_count
158
159    @property
160    def candidates_token_count(self) -> Optional[int]:
161        return self._candidates_token_count
162
163    @property
164    def total_token_count(self) -> Optional[int]:
165        return self._total_token_count
166
167    @property
168    def thoughts(self) -> Optional[str]:
169        return self._thoughts
170
171    @property
172    def json_dumps(self) -> str:
173        return self._json_dumps
174
175class APIKeyManager:
176    def __init__(self):
177        self.api_keys = re.findall(r"AIzaSy[a-zA-Z0-9_-]{33}", os.environ.get('KeyArray'))
178        self.current_index = random.randint(0, len(self.api_keys) - 1)
179
180    def get_available_key(self):
181        num_keys = len(self.api_keys)
182        for _ in range(num_keys):
183            if self.current_index >= num_keys:
184                self.current_index = 0
185            current_key = self.api_keys[self.current_index]
186            self.current_index += 1
187
188            if current_key not in api_key_blacklist:
189                return current_key
190
191        logger.error("所有API key都已耗尽或被暂时禁用,请重新配置或稍后重试")
192        return None
193
194    def show_all_keys(self):
195        logger.info(f"当前可用API key个数: {len(self.api_keys)} ")
196        for i, api_key in enumerate(self.api_keys):
197            logger.info(f"API Key{i}: {api_key[:8]}...{api_key[-3:]}")
198
199    def blacklist_key(self, key):
200        logger.warning(f"{key[:8]} → 暂时禁用 {api_key_blacklist_duration} 秒")
201        api_key_blacklist.add(key)
202
203        scheduler.add_job(lambda: api_key_blacklist.discard(key), 'date', run_date=datetime.now() + timedelta(seconds=api_key_blacklist_duration))
204
205key_manager = APIKeyManager()
206key_manager.show_all_keys()
207current_api_key = key_manager.get_available_key()
208
209def switch_api_key():
210    global current_api_key
211    key = key_manager.get_available_key()
212    if key:
213      current_api_key = key
214      logger.info(f"API key 替换为 → {current_api_key[:8]}...{current_api_key[-3:]}")
215    else:
216      logger.error("API key 替换失败,所有API key都已耗尽或被暂时禁用,请重新配置或稍后重试")
217
218logger.info(f"当前 API key: {current_api_key[:8]}...{current_api_key[-3:]}")
219
220GEMINI_MODELS = [
221    {"id": "text-embedding-004"},
222    {"id": "gemini-1.5-flash-8b-latest"},
223    {"id": "gemini-1.5-flash-8b-exp-0924"},
224    {"id": "gemini-1.5-flash-latest"},
225    {"id": "gemini-1.5-flash-exp-0827"},
226    {"id": "gemini-1.5-pro-latest"},
227    {"id": "gemini-1.5-pro-exp-0827"},
228    {"id": "learnlm-1.5-pro-experimental"},
229    {"id": "gemini-exp-1114"},
230    {"id": "gemini-exp-1121"},
231    {"id": "gemini-exp-1206"},
232    {"id": "gemini-2.0-flash-exp"},
233    {"id": "gemini-2.0-flash-thinking-exp-1219"},
234    {"id": "gemini-2.0-flash-thinking-exp-01-21"},
235    {"id": "gemini-2.0-pro-exp"}
236]
237
238@app.route('/')
239def index():
240    main_content = "Moonfanz Reminiproxy v2.3.5 2025-01-14"
241    html_template = """
242<!DOCTYPE html>
243<html>
244<head>
245<meta charset="utf-8">
246<script>
247function copyToClipboard(text) {
248  var textarea = document.createElement("textarea");
249  textarea.textContent = text;
250  textarea.style.position = "fixed";
251  document.body.appendChild(textarea);
252  textarea.select();
253  try {
254    return document.execCommand("copy");
255  } catch (ex) {
256    console.warn("Copy to clipboard failed.", ex);
257    return false;
258  } finally {
259    document.body.removeChild(textarea);
260  }
261}
262function copyLink(event) {
263  event.preventDefault();
264  const url = new URL(window.location.href);
265  const link = url.protocol + '//' + url.host + '/hf/v1';
266  copyToClipboard(link);
267  alert('链接已复制: ' + link);
268}
269</script>
270</head>
271<body>
272{{ main_content }}<br/><br/>完全开源、免费且禁止商用<br/><br/>点击复制反向代理: <a href="v1" onclick="copyLink(event)">点击我复制地址(不要自己写地址)</a><br/>聊天来源选择"自定义(兼容 OpenAI)"<br/>将复制的网址填入到自定义端点<br/>将设置password填入自定义API秘钥<br/><br/><br/>
273</body>
274</html>
275    """
276    return render_template_string(html_template, main_content=main_content)
277
278def is_within_rate_limit(api_key):
279    now = datetime.now()
280    if api_key not in request_counts:
281        request_counts[api_key] = deque()
282
283    while request_counts[api_key] and request_counts[api_key][0] < now - timedelta(seconds=LIMIT_WINDOW):
284        request_counts[api_key].popleft()
285
286    if len(request_counts[api_key]) >= MAX_REQUESTS:
287        earliest_request_time = request_counts[api_key][0]
288        wait_time = (earliest_request_time + timedelta(seconds=LIMIT_WINDOW)) - now
289        return False, wait_time.total_seconds()
290    else:
291        return True, 0
292
293def increment_request_count(api_key):
294    now = datetime.now()
295    if api_key not in request_counts:
296        request_counts[api_key] = deque()
297    request_counts[api_key].append(now)
298
299def handle_api_error(error, attempt, current_api_key):
300    if attempt > MAX_RETRIES:
301        logger.error(f"{MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入")
302        return 0, jsonify({
303            'error': {
304                'message': f"{MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入",
305                'type': 'max_retries_exceeded'
306            }
307        })
308
309    if isinstance(error, requests.exceptions.HTTPError):
310        status_code = error.response.status_code
311
312        if status_code == 400:
313
314            try:
315                error_data = error.response.json()
316                if 'error' in error_data:
317                    if error_data['error'].get('code') == "invalid_argument":
318                        logger.error(f"{current_api_key[:8]} ... {current_api_key[-3:]} → 无效,可能已过期或被删除")
319                        key_manager.blacklist_key(current_api_key)
320                        switch_api_key()
321                        return 0, None
322                    error_message = error_data['error'].get('message', 'Bad Request')
323                    error_type = error_data['error'].get('type', 'invalid_request_error')
324                    logger.warning(f"400 错误请求: {error_message}")
325                    return 2, jsonify({'error': {'message': error_message, 'type': error_type}})
326            except ValueError:
327                logger.warning("400 错误请求:响应不是有效的JSON格式")
328                return 2, jsonify({'error': {'message': '', 'type': 'invalid_request_error'}})
329
330        elif status_code == 429:
331            logger.warning(
332                f"{current_api_key[:8]} ... {current_api_key[-3:]} → 429 官方资源耗尽 → 立即重试..."
333            )
334            key_manager.blacklist_key(current_api_key)
335            switch_api_key()
336            return 0, None
337
338        elif status_code == 403:
339            logger.error(
340                f"{current_api_key[:8]} ... {current_api_key[-3:]} → 403 权限被拒绝,该 API KEY 可能已经被官方封禁"
341            )
342            key_manager.blacklist_key(current_api_key)
343            switch_api_key()
344            return 0, None
345
346        elif status_code == 500:
347            logger.warning(
348                f"{current_api_key[:8]} ... {current_api_key[-3:]} → 500 服务器内部错误 → 立即重试..."
349            )
350            switch_api_key()
351            return 0, None
352
353        elif status_code == 503:
354            logger.warning(
355                f"{current_api_key[:8]} ... {current_api_key[-3:]} → 503 服务不可用 → 立即重试..."
356            )
357            switch_api_key()
358            return 0, None
359
360        else:
361            logger.warning(
362                f"{current_api_key[:8]} ... {current_api_key[-3:]} → {status_code} 未知错误/模型不可用 → 不重试..."
363            )
364            switch_api_key()
365            return 2, None
366
367    elif isinstance(error, requests.exceptions.ConnectionError):
368        delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
369        logger.warning(f"连接错误 → 立即重试...")
370        time.sleep(delay)
371        return 0, None
372
373    elif isinstance(error, requests.exceptions.Timeout):
374        delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
375        logger.warning(f"请求超时 → 立即重试...")
376        time.sleep(delay)
377        return 0, None
378
379    else:
380        logger.error(f"发生未知错误: {error}")
381        return 0, jsonify({
382            'error': {
383                'message': f"发生未知错误: {error}",
384                'type': 'unknown_error'
385            }
386        })
387
388@app.route('/hf/v1/chat/completions', methods=['POST'])
389def chat_completions():
390    is_authenticated, auth_error, status_code = func.authenticate_request(request)
391    if not is_authenticated:
392        return auth_error if auth_error else jsonify({'error': '未授权'}), status_code if status_code else 401
393
394    request_data = request.get_json()
395    messages = request_data.get('messages', [])
396    model = request_data.get('model', 'gemini-2.0-flash-exp')
397    temperature = request_data.get('temperature', 1)
398    max_tokens = request_data.get('max_tokens', 8192)
399    show_thoughts = request_data.get('show_thoughts', False)
400    stream = request_data.get('stream', False)
401    use_system_prompt = request_data.get('use_system_prompt', False)
402    hint = "流式" if stream else "非流"
403    logger.info(f"\n{model} [{hint}] → {current_api_key[:8]}...{current_api_key[-3:]}")
404    is_thinking = 'thinking' in model
405    api_version = 'v1alpha' if is_thinking else 'v1beta'
406    response_type = 'streamGenerateContent' if stream else 'generateContent'
407    is_SSE = '&alt=sse' if stream else ''
408
409    contents, system_instruction, error_response = func.process_messages_for_gemini(messages, use_system_prompt)
410
411    if error_response:
412        logger.error(f"处理输入消息时出错↙\n {error_response}")
413        return jsonify(error_response), 400
414
415    def do_request(current_api_key, attempt):
416        isok, time_remaining = is_within_rate_limit(current_api_key)
417        if not isok:
418            logger.warning(f"暂时超过限额,该API key将在 {time_remaining} 秒后启用...")
419            switch_api_key()
420            return 0, None
421
422        increment_request_count(current_api_key)
423
424
425        url = f"https://generativelanguage.googleapis.com/{api_version}/models/{model}:{response_type}?key={current_api_key}{is_SSE}"
426        headers = {
427            "Content-Type": "application/json",
428        }
429
430        data = {
431            "contents": contents,
432            "generationConfig": {
433                "temperature": temperature,
434                "maxOutputTokens": max_tokens,
435            },
436            "safetySettings": safety_settings_g2 if 'gemini-2.0-flash-exp' in model else safety_settings,
437        }
438        if system_instruction:
439            data["system_instruction"] = system_instruction
440
441        try:
442            response = requests.post(url, headers=headers, json=data, stream=True)
443            response.raise_for_status()
444
445            if stream:
446                return 1, response
447            else:
448                return 1, ResponseWrapper(response.json())
449        except requests.exceptions.RequestException as e:
450            return handle_api_error(e, attempt, current_api_key)
451
452    def generate_stream(response):
453        logger.info(f"流式开始 →")
454        buffer = b""
455        try:
456            for line in response.iter_lines():
457                if not line:
458                    continue
459                try:
460                    if line.startswith(b'data: '):
461                        line = line[6:]
462
463                    buffer += line
464
465                    try:
466                        data = json.loads(buffer.decode('utf-8'))
467                        buffer = b""
468                        if 'candidates' in data and data['candidates']:
469                            candidate = data['candidates'][0]
470                            if 'content' in candidate:
471                                content = candidate['content']
472                                if 'parts' in content and content['parts']:
473                                    parts = content['parts']
474                                    if is_thinking and not show_thoughts:
475                                        parts = [part for part in parts if not part.get('thought')]
476                                    if parts:
477                                        text = parts[0].get('text', '')
478                                        finish_reason = candidate.get('finishReason')
479
480                                        if text:
481                                            data = {
482                                                'choices': [{
483                                                    'delta': {
484                                                        'content': text
485                                                    },
486                                                    'finish_reason': finish_reason,
487                                                    'index': 0
488                                                }],
489                                                'object': 'chat.completion.chunk'
490                                            }
491                                            yield f"data: {json.dumps(data)}\n\n"
492
493                            if candidate.get("finishReason") and candidate.get("finishReason") != "STOP":
494                                error_message = {
495                                    "error": {
496                                        "code": "content_filter",
497                                        "message": f"模型的响应因违反内容政策而被标记:{candidate.get('finishReason')}",
498                                        "status": candidate.get("finishReason"),
499                                        "details": []
500                                    }
501                                }
502                                logger.warning(f"模型的响应因违反内容政策而被标记: {candidate.get('finishReason')}")
503                                yield f"data: {json.dumps(error_message)}\n\n"
504                                break
505
506                            if 'safetyRatings' in candidate:
507                                for rating in candidate['safetyRatings']:
508                                    if rating['probability'] == 'HIGH':
509                                        error_message = {
510                                            "error": {
511                                                "code": "content_filter",
512                                                "message": f"模型的响应因高概率被标记为 {rating['category']}",
513                                                "status": "SAFETY_RATING_HIGH",
514                                                "details": [rating]
515                                            }
516                                        }
517                                        logger.warning(f"模型的响应因高概率被标记为 {rating['category']}")
518                                        yield f"data: {json.dumps(error_message)}\n\n"
519                                        break
520                                else:
521                                    continue
522                                break
523
524                    except json.JSONDecodeError:
525                        logger.debug(f"JSON解析错误, 当前缓冲区内容: {buffer}")
526                        continue
527
528                except Exception as e:
529                    logger.error(f"流式处理期间发生错误: {e}, 原始数据行↙\n{line}")
530                    yield f"data: {json.dumps({'error': str(e)})}\n\n"
531
532            else:
533                yield f"data: {json.dumps({'choices': [{'delta': {}, 'finish_reason': 'stop', 'index': 0}]})}\n\n"
534                logger.info(f"流式结束 ←")
535                logger.info(f"200!")
536        except Exception as e:
537            logger.error(f"流式处理错误↙\n{e}")
538            yield f"data: {json.dumps({'error': str(e)})}\n\n"
539
540    attempt = 0
541    success = 0
542    response = None
543    for attempt in range(1, MAX_RETRIES + 1):
544        logger.info(f"第 {attempt}/{MAX_RETRIES} 次尝试 ...")
545        success, response = do_request(current_api_key, attempt)
546
547        if success == 0:
548            continue
549        elif success == 1 and response is None:
550            continue
551        elif success == 1 and stream:
552            return Response(
553                stream_with_context(generate_stream(response)),
554                mimetype='text/event-stream'
555            )
556        elif success == 1 and isinstance(response, ResponseWrapper):
557            try:
558                text_content = response.text
559                prompt_tokens = response.prompt_token_count
560                completion_tokens = response.candidates_token_count
561                total_tokens = response.total_token_count
562                finish_reason = response.finish_reason
563
564                if text_content == '':
565                    error_message = None
566                    if response._data and 'error' in response._data:
567                        error_message = response._data['error'].get('message')
568                    if error_message:
569                        logger.error(f"生成内容失败,API 返回错误: {error_message}")
570                    else:
571                        logger.error(f"生成内容失败: text_content 为空")
572                    continue
573
574                if is_thinking and show_thoughts:
575                    text_content = response.thoughts + '\n' + text_content
576
577            except AttributeError as e:
578                logger.error(f"处理响应失败,缺少必要的属性: {e}")
579                logger.error(f"原始响应: {response._data}")
580                continue
581
582            except Exception as e:
583                logger.error(f"处理响应失败: {e}")
584                continue
585
586            response_data = {
587                'id': 'chatcmpl-xxxxxxxxxxxx',
588                'object': 'chat.completion',
589                'created': int(datetime.now().timestamp()),
590                'model': model,
591                'choices': [{
592                    'index': 0,
593                    'message': {
594                        'role': 'assistant',
595                        'content': text_content
596                    },
597                    'finish_reason': finish_reason
598                }],
599                'usage': {
600                    'prompt_tokens': prompt_tokens,
601                    'completion_tokens': completion_tokens,
602                    'total_tokens': total_tokens
603                }
604            }
605            logger.info(f"200!")
606            return jsonify(response_data)
607        elif success == 1 and isinstance(response, tuple):
608            return response[1], response[0]
609        elif success == 2:
610            logger.error(f"{model} 可能暂时不可用,请更换模型或未来一段时间再试")
611            response = {
612                'error': {
613                    'message': f'{model} 可能暂时不可用,请更换模型或未来一段时间再试',
614                    'type': 'internal_server_error'
615                }
616            }
617            return jsonify(response), 503
618    else:
619        logger.error(f"{MAX_RETRIES} 次尝试均失败,请重试或等待官方恢复")
620        response = {
621            'error': {
622                'message': f'{MAX_RETRIES} 次尝试均失败,请重试或等待官方恢复',
623                'type': 'internal_server_error'
624            }
625        }
626        return jsonify(response), 500 if response is not None else 503
627
628@app.route('/hf/v1/models', methods=['GET'])
629def list_models():
630    response = {"object": "list", "data": GEMINI_MODELS}
631    return jsonify(response)
632
633@app.route('/hf/v1/embeddings', methods=['POST'])
634def embeddings():
635    data = request.get_json()
636    model_input = data.get("input")
637    model = data.get("model", "text-embedding-004")
638    if not model_input:
639        return jsonify({"error": "没有提供输入"}), 400
640
641    if isinstance(model_input, str):
642        model_input = [model_input]
643
644    gemini_request = {
645        "model": f"models/{model}",
646        "content": {
647            "parts": [{"text": text} for text in model_input]
648        }
649    }
650
651    gemini_url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:embedContent?key={current_api_key}"
652    headers = {"Content-Type": "application/json"}
653    try:
654        gemini_response = requests.post(gemini_url, json=gemini_request, headers=headers)
655        gemini_response.raise_for_status()
656
657        response_json = gemini_response.json()
658        embeddings_data = []
659        if 'embedding' in response_json:
660          embeddings_data.append({
661              "object": "embedding",
662              "embedding": response_json['embedding']['values'],
663              "index": 0,
664          })
665        elif 'embeddings' in response_json:
666          for i, embedding in enumerate(response_json['embeddings']):
667              embeddings_data.append({
668                  "object": "embedding",
669                  "embedding": embedding['values'],
670                  "index": i,
671              })
672
673        client_response = {
674            "object": "list",
675            "data": embeddings_data,
676            "model": model,
677            "usage": {
678                "prompt_tokens": 0,
679                "total_tokens": 0,
680            },
681        }
682        switch_api_key()
683        return jsonify(client_response)
684
685    except requests.exceptions.RequestException as e:
686        print(f"请求Embeddings失败↙\: {e}")
687        return jsonify({"error": str(e)}), 500
688
689def keep_alive():
690    try:
691        response = requests.get("http://127.0.0.1:7860/", timeout=10)
692        response.raise_for_status()  
693        print(f"Keep alive ping successful: {response.status_code} at {time.ctime()}")
694    except requests.exceptions.RequestException as e:
695        print(f"Keep alive ping failed: {e} at {time.ctime()}")
696
697if __name__ == '__main__':
698    scheduler = BackgroundScheduler()
699
700    scheduler.add_job(keep_alive, 'interval', hours=12)
701    scheduler.start()
702    logger.info(f"Reminiproxy v2.3.5 启动")
703    logger.info(f"最大尝试次数/MaxRetries: {MAX_RETRIES}")
704    logger.info(f"最大请求次数/MaxRequests: {MAX_REQUESTS}")
705    logger.info(f"请求限额窗口/LimitWindow: {LIMIT_WINDOW} 秒")
706
707    app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))