├── .gitignore ├── app.py ├── config_editor.bat ├── config_editor.py ├── docker-compose.yaml ├── dockerfile ├── requirements.txt ├── start.bat └── start.sh /.gitignore: -------------------------------------------------------------------------------- 1 | config.* 2 | config.* 3 | settings.json 4 | message_log.txt 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # UV 102 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | #uv.lock 106 | 107 | # poetry 108 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 109 | # This is especially recommended for binary packages to ensure reproducibility, and is more 110 | # commonly ignored for libraries. 111 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 112 | #poetry.lock 113 | 114 | # pdm 115 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 116 | #pdm.lock 117 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 118 | # in version control. 119 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 120 | .pdm.toml 121 | .pdm-python 122 | .pdm-build/ 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | #.idea/ 173 | 174 | # Ruff stuff: 175 | .ruff_cache/ 176 | 177 | # PyPI configuration file 178 | .pypirc -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from flask import Flask, request, jsonify, Response 3 | import requests 4 | import io 5 | import json 6 | import re 7 | import uuid 8 | import random 9 | import time 10 | from functools import wraps 11 | 12 | app = Flask(__name__) 13 | 14 | TARGET_URL = "https://grok.com/rest/app-chat/conversations/new" 15 | CHECK_URL = "https://grok.com/rest/rate-limits" 16 | MODELS = ["grok-2", "grok-3", "grok-3-thinking"] 17 | CONFIG = {} 18 | TEMPORARY_MODE = False 19 | COOKIE_NUM = 0 20 | COOKIE_LIST = [] 21 | LAST_COOKIE_INDEX = {} 22 | PASSWORD = "" 23 | 24 | USER_AGENTS = [ 25 | # Windows - Chrome 26 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", 27 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", 28 | # Windows - Firefox 29 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0", 30 | # Windows - Edge 31 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.2420.81", 32 | # Windows - Opera 33 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 OPR/109.0.0.0", 34 | # macOS - Chrome 35 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", 36 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", 37 | # macOS - Safari 38 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Safari/605.1.15", 39 | # macOS - Firefox 40 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:132.0) Gecko/20100101 Firefox/132.0", 41 | # macOS - Opera 42 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 OPR/109.0.0.0", 43 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.4; rv:124.0) Gecko/20100101 Firefox/124.0", 44 | # Linux - Chrome 45 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", 46 | # Linux - Firefox 47 | "Mozilla/5.0 (X11; Linux i686; rv:124.0) Gecko/20100101 Firefox/124.0", 48 | ] 49 | 50 | 51 | def resolve_config(): 52 | global COOKIE_NUM, COOKIE_LIST, LAST_COOKIE_INDEX, TEMPORARY_MODE, CONFIG, PASSWORD 53 | with open("config.json", "r") as f: 54 | CONFIG = json.load(f) 55 | for cookies in CONFIG["cookies"]: 56 | session = requests.Session() 57 | session.headers.update( 58 | {"user-agent": random.choice(USER_AGENTS), "cookie": cookies} 59 | ) 60 | 61 | COOKIE_LIST.append(session) 62 | COOKIE_NUM = len(COOKIE_LIST) 63 | TEMPORARY_MODE = CONFIG["temporary_mode"] 64 | for model in MODELS: 65 | LAST_COOKIE_INDEX[model] = CONFIG["last_cookie_index"][model] 66 | PASSWORD = CONFIG.get("password", "") 67 | 68 | 69 | def require_auth(f): 70 | @wraps(f) 71 | def decorated(*args, **kwargs): 72 | if not PASSWORD: 73 | return f(*args, **kwargs) 74 | auth = request.authorization 75 | if not auth or not check_auth(auth.token): 76 | return jsonify({"error": "Unauthorized access"}), 401 77 | return f(*args, **kwargs) 78 | 79 | return decorated 80 | 81 | 82 | def check_auth(password): 83 | return hashlib.sha256(password.encode()).hexdigest() == PASSWORD 84 | 85 | 86 | @app.route("/v1/models", methods=["GET"]) 87 | @require_auth 88 | def get_models(): 89 | model_list = [] 90 | for model in MODELS: 91 | model_list.append( 92 | { 93 | "id": model, 94 | "object": "model", 95 | "created": int(time.time()), 96 | "owned_by": "Elbert", 97 | "name": model, 98 | } 99 | ) 100 | return jsonify({"object": "list", "data": model_list}) 101 | 102 | 103 | @app.route("/v1/chat/completions", methods=["POST"]) 104 | @require_auth 105 | def chat_completions(): 106 | print("Received request") 107 | openai_request = request.get_json() 108 | print(openai_request) 109 | stream = openai_request.get("stream", False) 110 | messages = openai_request.get("messages") 111 | model = openai_request.get("model") 112 | if model not in MODELS: 113 | return jsonify({"error": "Model not available"}), 500 114 | if messages is None: 115 | return jsonify({"error": "Messages is required"}), 400 116 | disable_search, force_concise, messages = magic(messages) 117 | message = format_message(messages) 118 | is_reasoning = len(model) > 6 119 | model = model[0:6] 120 | return ( 121 | send_message(message, model, disable_search, force_concise, is_reasoning) 122 | if stream 123 | else send_message_non_stream( 124 | message, model, disable_search, force_concise, is_reasoning 125 | ) 126 | ) 127 | 128 | 129 | def get_next_account(model): 130 | current = (LAST_COOKIE_INDEX[model] + 1) % COOKIE_NUM 131 | LAST_COOKIE_INDEX[model] = current 132 | print(f"Using account {current+1}/{COOKIE_NUM} for {model}") 133 | CONFIG["last_cookie_index"][model] = current 134 | with open("config.json", "w") as f: 135 | json.dump(CONFIG, f, indent=4) 136 | return COOKIE_LIST[current] 137 | 138 | 139 | def send_message(message, model, disable_search, force_concise, is_reasoning): 140 | headers = { 141 | "authority": "grok.com", 142 | "method": "POST", 143 | "path": "/rest/app-chat/conversations/new", 144 | "scheme": "https", 145 | "accept": "*/*", 146 | "accept-encoding": "gzip, deflate, br, zstd", 147 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", 148 | "cache-control": "no-cache", 149 | "content-type": "application/json", 150 | "origin": "https://grok.com", 151 | "pragma": "no-cache", 152 | "priority": "u=1, i", 153 | "referer": "https://grok.com/", 154 | "sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"', 155 | "sec-ch-ua-mobile": "?0", 156 | "sec-ch-ua-platform": '"Windows"', 157 | "sec-fetch-dest": "empty", 158 | "sec-fetch-mode": "cors", 159 | "sec-fetch-site": "same-origin", 160 | } 161 | payload = { 162 | "temporary": TEMPORARY_MODE, 163 | "modelName": "grok-3", 164 | "message": message, 165 | "fileAttachments": [], 166 | "imageAttachments": [], 167 | "disableSearch": disable_search, 168 | "enableImageGeneration": False, 169 | "returnImageBytes": False, 170 | "returnRawGrokInXaiRequest": False, 171 | "enableImageStreaming": True, 172 | "imageGenerationCount": 2, 173 | "forceConcise": force_concise, 174 | "toolOverrides": {}, 175 | "enableSideBySide": True, 176 | "isPreset": False, 177 | "sendFinalMetadata": True, 178 | "customInstructions": "", 179 | "deepsearchPreset": "", 180 | "isReasoning": is_reasoning, 181 | } 182 | session = get_next_account(model) 183 | try: 184 | response = session.post(TARGET_URL, headers=headers, json=payload, stream=True) 185 | response.raise_for_status() 186 | 187 | def generate(): 188 | try: 189 | print("---------- Response ----------") 190 | cnt = 2 191 | thinking = 2 192 | for line in response.iter_lines(): 193 | if line: 194 | if cnt != 0: 195 | cnt -= 1 196 | else: 197 | decoded_line = line.decode("utf-8") 198 | data = json.loads(decoded_line) 199 | token = data["result"]["response"]["token"] 200 | content = "" 201 | if is_reasoning: 202 | if thinking == 2: 203 | thinking = 1 204 | content = f"\n{token}" 205 | print(f"{content}", end="") 206 | elif thinking & ( 207 | not data["result"]["response"]["isThinking"] 208 | ): 209 | thinking = 0 210 | content = f"\n\n{token}" 211 | print(f"{content}", end="") 212 | else: 213 | content = token 214 | print(content, end="") 215 | else: 216 | content = token 217 | print(content, end="") 218 | openai_chunk = { 219 | "id": "chatcmpl-" + str(uuid.uuid4()), 220 | "object": "chat.completion.chunk", 221 | "created": int(time.time()), 222 | "model": model, 223 | "choices": [ 224 | { 225 | "index": 0, 226 | "delta": {"content": content}, 227 | "finish_reason": None, 228 | } 229 | ], 230 | } 231 | yield f"data: {json.dumps(openai_chunk)}\n\n" 232 | if data["result"]["response"]["isSoftStop"]: 233 | openai_chunk = { 234 | "id": "chatcmpl-" + str(uuid.uuid4()), 235 | "object": "chat.completion.chunk", 236 | "created": int(time.time()), 237 | "model": model, 238 | "choices": [ 239 | { 240 | "index": 0, 241 | "delta": {"content": content}, 242 | "finish_reason": "completed", 243 | } 244 | ], 245 | } 246 | yield f"data: {json.dumps(openai_chunk)}\n\n" 247 | break 248 | print("\n---------- Response End ----------") 249 | yield f"data: [DONE]\n\n" 250 | except Exception as e: 251 | print(f"Failed to send message: {e}") 252 | yield f'data: {{"error": "{e}", "status": {response.status_code}}}\n\n' 253 | 254 | return Response(generate(), content_type="text/event-stream") 255 | except requests.exceptions.RequestException as e: 256 | print(f"Failed to send message: {e}") 257 | return jsonify({"error": f"{e}", "status": response.status_code}) 258 | 259 | 260 | def send_message_non_stream( 261 | message, model, disable_search, force_concise, is_reasoning 262 | ): 263 | headers = { 264 | "authority": "grok.com", 265 | "method": "POST", 266 | "path": "/rest/app-chat/conversations/new", 267 | "scheme": "https", 268 | "accept": "*/*", 269 | "accept-encoding": "gzip, deflate, br, zstd", 270 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", 271 | "cache-control": "no-cache", 272 | "content-type": "application/json", 273 | "origin": "https://grok.com", 274 | "pragma": "no-cache", 275 | "priority": "u=1, i", 276 | "referer": "https://grok.com/", 277 | "sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"', 278 | "sec-ch-ua-mobile": "?0", 279 | "sec-ch-ua-platform": '"Windows"', 280 | "sec-fetch-dest": "empty", 281 | "sec-fetch-mode": "cors", 282 | "sec-fetch-site": "same-origin", 283 | } 284 | payload = { 285 | "temporary": TEMPORARY_MODE, 286 | "modelName": "grok-3", 287 | "message": message, 288 | "fileAttachments": [], 289 | "imageAttachments": [], 290 | "disableSearch": disable_search, 291 | "enableImageGeneration": False, 292 | "returnImageBytes": False, 293 | "returnRawGrokInXaiRequest": False, 294 | "enableImageStreaming": True, 295 | "imageGenerationCount": 2, 296 | "forceConcise": force_concise, 297 | "toolOverrides": {}, 298 | "enableSideBySide": True, 299 | "isPreset": False, 300 | "sendFinalMetadata": True, 301 | "customInstructions": "", 302 | "deepsearchPreset": "", 303 | "isReasoning": is_reasoning, 304 | } 305 | session = get_next_account(model) 306 | thinking = 2 307 | try: 308 | response = session.post(TARGET_URL, headers=headers, json=payload, stream=True) 309 | response.raise_for_status() 310 | cnt = 2 311 | try: 312 | print("---------- Response ----------") 313 | buffer = io.StringIO() 314 | for line in response.iter_lines(): 315 | if line: 316 | if cnt != 0: 317 | cnt -= 1 318 | else: 319 | decoded_line = line.decode("utf-8") 320 | data = json.loads(decoded_line) 321 | token = data["result"]["response"]["token"] 322 | content = "" 323 | if is_reasoning: 324 | if thinking == 2: 325 | thinking = 1 326 | content = f"\n{token}" 327 | print(f"{content}", end="") 328 | buffer.write(content) 329 | elif thinking & ( 330 | not data["result"]["response"]["isThinking"] 331 | ): 332 | thinking = 0 333 | content = f"\n\n{token}" 334 | print(f"{content}", end="") 335 | buffer.write(content) 336 | else: 337 | content = token 338 | print(content, end="") 339 | buffer.write(content) 340 | else: 341 | content = token 342 | print(content, end="") 343 | buffer.write(content) 344 | if data["result"]["response"]["isSoftStop"]: 345 | break 346 | print("\n---------- Response End ----------") 347 | openai_response = { 348 | "id": "chatcmpl-" + str(uuid.uuid4()), 349 | "object": "chat.completion", 350 | "created": int(time.time()), 351 | "model": model, 352 | "choices": [ 353 | { 354 | "index": 0, 355 | "message": {"role": "assistant", "content": buffer.getvalue()}, 356 | "finish_reason": "completed", 357 | } 358 | ], 359 | } 360 | return jsonify(openai_response) 361 | except Exception as e: 362 | print(f"Failed to send message: {e}") 363 | return jsonify({"error": f"{e}", "status": response.status_code}) 364 | except requests.exceptions.RequestException as e: 365 | print(f"Failed to send message: {e}") 366 | return jsonify({"error": f"{e}", "status": response.status_code}) 367 | 368 | 369 | def format_message(messages): 370 | buffer = io.StringIO() 371 | role_map, prefix, messages = extract_role(messages) 372 | for message in messages: 373 | role = message.get("role") 374 | role = "\b" + role_map[role] if prefix else role_map[role] 375 | content = message.get("content").replace("\\n", "\n") 376 | pattern = re.compile(r"<\|removeRole\|>\n") 377 | if pattern.match(content): 378 | content = pattern.sub("", content) 379 | buffer.write(f"{content}\n") 380 | else: 381 | buffer.write(f"{role}: {content}\n\n") 382 | formatted_message = buffer.getvalue() 383 | with open("message_log.txt", "w", encoding="utf-8") as f: 384 | f.write(formatted_message) 385 | return formatted_message 386 | 387 | 388 | def extract_role(messages): 389 | role_map = {"user": "Human", "assistant": "Assistant", "system": "System"} 390 | prefix = False 391 | first_message = messages[0]["content"] 392 | pattern = re.compile( 393 | r""" 394 | \s* 395 | user:\s*(?P[^\n]*)\s* 396 | assistant:\s*(?P[^\n]*)\s* 397 | system:\s*(?P[^\n]*)\s* 398 | prefix:\s*(?P[^\n]*)\s* 399 | \n 400 | """, 401 | re.VERBOSE, 402 | ) 403 | match = pattern.search(first_message) 404 | if match: 405 | role_map = { 406 | "user": match.group("user"), 407 | "assistant": match.group("assistant"), 408 | "system": match.group("system"), 409 | } 410 | prefix = match.group("prefix") == "1" 411 | messages[0]["content"] = pattern.sub("", first_message) 412 | print(f"Extracted role map:") 413 | print( 414 | f"User: {role_map['user']}, Assistant: {role_map['assistant']}, System: {role_map['system']}" 415 | ) 416 | print(f"Using prefix: {prefix}") 417 | return (role_map, prefix, messages) 418 | 419 | 420 | def magic(messages): 421 | first_message = messages[0]["content"] 422 | disable_search = False 423 | if re.search(r"<\|disableSearch\|>", first_message): 424 | disable_search = True 425 | print("Disable search") 426 | first_message = re.sub(r"<\|disableSearch\|>", "", first_message) 427 | force_concise = False 428 | if re.search(r"<\|forceConcise\|>", first_message): 429 | force_concise = True 430 | print("Force concise") 431 | first_message = re.sub(r"<\|forceConcise\|>", "", first_message) 432 | messages[0]["content"] = first_message 433 | return (disable_search, force_concise, messages) 434 | 435 | 436 | def check_rate_limit(session, model, is_reasoning): 437 | headers = { 438 | "authority": "grok.com", 439 | "method": "POST", 440 | "path": "/rest/rate-limits", 441 | "scheme": "https", 442 | "accept": "*/*", 443 | "accept-encoding": "gzip, deflate, br, zstd", 444 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", 445 | "cache-control": "no-cache", 446 | "content-type": "application/json", 447 | "origin": "https://grok.com", 448 | "pragma": "no-cache", 449 | "priority": "u=1, i", 450 | "referer": "https://grok.com/", 451 | "sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"', 452 | "sec-ch-ua-mobile": "?0", 453 | "sec-ch-ua-platform": '"Windows"', 454 | "sec-fetch-dest": "empty", 455 | "sec-fetch-mode": "cors", 456 | "sec-fetch-site": "same-origin", 457 | } 458 | payload = { 459 | "requestKind": "REASONING" if is_reasoning else "DEFAULT", 460 | "modelName": model, 461 | } 462 | try: 463 | response = session.post(CHECK_URL, headers=headers, json=payload) 464 | response.raise_for_status() 465 | data = json.loads(response.content) 466 | if data["remainingQueries"] != 0: 467 | return (True, data["remainingQueries"]) 468 | else: 469 | available_time = time.time() + data["waitTimeSeconds"] 470 | return (False, available_time) 471 | 472 | except Exception as e: 473 | print(f"Failed to check rate limit: {e}") 474 | return (False, None) 475 | 476 | 477 | resolve_config() 478 | 479 | if __name__ == "__main__": 480 | app.run(host="0.0.0.0", port=9898) 481 | -------------------------------------------------------------------------------- /config_editor.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: 设置控制台编码为UTF-8 3 | chcp 65001 > nul 4 | 5 | :: 设置文件名 6 | set "filename=config.txt" 7 | 8 | :: 检测文件是否存在 9 | if exist "%filename%" ( 10 | echo 检测到当前目录下已存在 %filename% 文件。 11 | echo. 12 | call :ask_confirm "是否覆盖现有文件?(Y/N)" 13 | if /i "%confirm%"=="n" ( 14 | echo 已取消操作,文件未修改。 15 | pause 16 | exit /b 17 | ) 18 | ) 19 | 20 | :: 初始化 cookies 数组 21 | set "cookies=[" 22 | 23 | :: 循环输入 cookie 24 | :input_cookie 25 | set "new_cookie=" 26 | set /p new_cookie=请输入 cookie 的内容(或直接按回车结束输入): 27 | if "%new_cookie%"=="" goto end_input 28 | 29 | :: 将新 cookie 添加到 cookies 数组 30 | if "%cookies%"=="[" ( 31 | set "cookies=%cookies%"%new_cookie%"" 32 | ) else ( 33 | set "cookies=%cookies%, "%new_cookie%"" 34 | ) 35 | 36 | :: 询问是否继续输入 37 | call :ask_confirm "是否继续输入下一个 cookie?(Y/N)" 38 | if /i "%confirm%"=="n" goto end_input 39 | goto input_cookie 40 | 41 | :end_input 42 | :: 完成 cookies 数组 43 | set "cookies=%cookies%]" 44 | 45 | :: 生成 JSON 内容并写入文件 46 | echo {"cookies": %cookies%, "last_cookie_index": {"grok-2": 0, "grok-3": 0, "grok-3-thinking": 0}, "temporary_mode": true} > %filename% 47 | 48 | :: 检查文件是否写入成功 49 | if errorlevel 1 ( 50 | echo 错误:文件写入失败! 51 | pause 52 | exit /b 53 | ) 54 | 55 | :: 提示完成 56 | echo. 57 | echo 文件 %filename% 已成功创建/更新! 58 | 59 | :: 修改文件后缀名为 .json 60 | set "new_filename=config.json" 61 | ren "%filename%" "%new_filename%" 62 | 63 | :: 检查重命名是否成功 64 | if errorlevel 1 ( 65 | echo 错误:文件重命名失败! 66 | pause 67 | exit /b 68 | ) 69 | 70 | echo. 71 | echo 文件已重命名为 %new_filename%。 72 | pause 73 | exit /b 74 | 75 | :: 自定义函数:通过回车确认 Y/N 76 | :ask_confirm 77 | set "confirm=" 78 | set /p confirm=%~1 79 | if "%confirm%"=="" set "confirm=y" :: 默认回车为 Y 80 | if /i "%confirm%"=="y" set "confirm=y" 81 | if /i "%confirm%"=="n" set "confirm=n" 82 | if /i "%confirm%" neq "y" if /i "%confirm%" neq "n" ( 83 | echo 输入无效,请输入 Y 或 N。 84 | goto ask_confirm 85 | ) 86 | exit /b 87 | -------------------------------------------------------------------------------- /config_editor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import hashlib 4 | 5 | # 获取项目根目录 6 | project_root = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | # config.json 文件的路径 9 | config_file_path = os.path.join(project_root, "config.json") 10 | 11 | if __name__ == "__main__": 12 | if not os.path.exists(config_file_path): 13 | print(f"config.json not found or empty, creating...") 14 | config = { 15 | "cookies": [], 16 | "last_cookie_index": { 17 | "grok-2": 0, 18 | "grok-3": 0, 19 | "grok-3-thinking": 0, 20 | }, 21 | "temporary_mode": True, 22 | "password": None, 23 | } 24 | print(f"Enter the cookies you got: ") 25 | config["cookies"].append(input()) 26 | else: 27 | with open(config_file_path, "r") as f: 28 | config = json.load(f) 29 | again = True 30 | while True: 31 | if again: 32 | num = len(config["cookies"]) 33 | print(f"You have {num} cookies in your config.json file.") 34 | print("----------") 35 | print(f"1. Add") 36 | print(f"2. Delete all") 37 | print(f"3. Toggle temporary mode") 38 | print(f"4. Set password") 39 | print(f"5. Save and exit") 40 | choice = input() 41 | if choice == "1": 42 | print(f"Enter the cookies you got: ") 43 | config["cookies"].append(input()) 44 | again = True 45 | elif choice == "2": 46 | config["cookies"] = [] 47 | print(f"Deleted all cookies, enter new cookies:") 48 | config["cookies"].append(input()) 49 | again = True 50 | elif choice == "3": 51 | config["temporary_mode"] = not config["temporary_mode"] 52 | print( 53 | f"Temporary mode is now {'on' if config['temporary_mode'] else 'off'}" 54 | ) 55 | again = False 56 | elif choice == "4": 57 | print(f"Enter the password, leave empty to clear:") 58 | password = input() 59 | if password: 60 | config["password"] = hashlib.sha256(password.encode()).hexdigest() 61 | print(f"Password set.") 62 | else: 63 | config.pop("password", None) 64 | print(f"Password cleared.") 65 | elif choice == "5": 66 | with open(config_file_path, "w", encoding="utf-8") as f: 67 | json.dump(config, f, ensure_ascii=False) 68 | break 69 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | grok: 5 | image: grok:latest 6 | build: . 7 | container_name: grok_chat_proxy 8 | restart: unless-stopped 9 | ports: 10 | - "9898:9898" 11 | volumes: 12 | - ./config.json:/app/config.json 13 | environment: 14 | - PYTHONUNBUFFERED=1 15 | networks: 16 | - grok_net 17 | 18 | networks: 19 | grok_net: 20 | driver: bridge 21 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | # 使用 Python 3.12.9-slim-bullseye 作為基礎映像 2 | FROM python:3.12.9-slim-bullseye 3 | 4 | # 設定工作目錄 5 | WORKDIR /app 6 | 7 | # 設定 Python 環境變數 8 | ENV PYTHONDONTWRITEBYTECODE=1 \ 9 | PYTHONUNBUFFERED=1 \ 10 | LANG=C.UTF-8 \ 11 | LC_ALL=C.UTF-8 12 | 13 | # 安裝系統依賴 14 | RUN apt-get update \ 15 | && apt-get install -y --no-install-recommends \ 16 | curl \ 17 | build-essential \ 18 | && rm -rf /var/lib/apt/lists/* 19 | 20 | # 複製專案檔案 21 | COPY . . 22 | 23 | # 安裝 Python 依賴 24 | RUN pip install --no-cache-dir -r requirements.txt 25 | 26 | # 設定容器對外埠號 27 | EXPOSE 9898 28 | 29 | # 設定啟動命令 30 | CMD ["python", "app.py"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | brotli==1.1.0 2 | Flask==3.1.0 3 | Requests==2.32.3 4 | -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > nul 3 | 4 | echo checking python version... 5 | python --version > nul 2>&1 6 | if errorlevel 1 ( 7 | echo No python found. Please install python and add it to the PATH. 8 | echo After installation, please restart the command prompt. 9 | pause 10 | exit /b 11 | ) 12 | 13 | echo checking pip version... 14 | pip --version > nul 2>&1 15 | if errorlevel 1 ( 16 | echo No pip found. Please install pip. 17 | echo After installation, please restart the command prompt. 18 | pause 19 | exit /b 20 | ) 21 | 22 | echo checking venv module... 23 | python -m venv venv 24 | if errorlevel 1 ( 25 | echo No venv module found. Please upgrade your python to 3.3+. 26 | pause 27 | exit /b 28 | ) 29 | 30 | echo activating virtual environment... 31 | call venv\Scripts\activate.bat 32 | if errorlevel 1 ( 33 | echo Failed to activate virtual environment, trying to install dependencies in global environment... 34 | goto install_dependencies 35 | ) 36 | 37 | echo virtual environment activated. 38 | 39 | :install_dependencies 40 | echo installing dependencies... 41 | pip install -r requirements.txt 42 | if errorlevel 1 ( 43 | echo Failed to install dependencies. Please check your network connection and try again. 44 | pause 45 | exit /b 46 | ) 47 | 48 | echo dependencies installed. 49 | 50 | echo starting Flask app... 51 | python app.py 52 | if errorlevel 1 ( 53 | echo Failed to start Flask app. Please check whether the port is occupied and whether the configuration is correct. 54 | pause 55 | exit /b 56 | ) 57 | 58 | echo Flask app listening on http://127.0.0.1:9898/ 59 | 60 | pause 61 | exit /b -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 检查 Python 是否已安装 4 | if ! command -v python3 &> /dev/null; then 5 | echo "Python 3 未安装。请先安装 Python 3.7 或更高版本。" 6 | echo "安装完成后,重新运行此脚本。" 7 | exit 1 8 | fi 9 | 10 | # 检查 pip 是否已安装 11 | if ! command -v pip3 &> /dev/null; then 12 | echo "pip3 未安装。请确保 pip 已正确安装并添加到环境变量。" 13 | exit 1 14 | fi 15 | 16 | # 创建虚拟环境(如果不存在) 17 | if [ ! -d "venv" ]; then 18 | echo "正在创建虚拟环境..." 19 | python3 -m venv venv 20 | if [ $? -ne 0 ]; then 21 | echo "创建虚拟环境失败。请检查是否已安装 venv 模块。" 22 | exit 1 23 | fi 24 | fi 25 | 26 | # 激活虚拟环境 27 | echo "正在激活虚拟环境..." 28 | source venv/bin/activate 29 | if [ $? -ne 0 ]; then 30 | echo "无法激活虚拟环境,尝试使用全局环境安装依赖" 31 | else 32 | echo "虚拟环境激活成功" 33 | fi 34 | 35 | # 安装依赖 36 | echo "正在安装依赖..." 37 | pip3 install -r requirements.txt 38 | if [ $? -ne 0 ]; then 39 | echo "安装依赖失败。请检查网络连接和 requirements.txt 文件。" 40 | exit 1 41 | fi 42 | 43 | echo "依赖安装成功。" 44 | 45 | # 启动 Flask 应用 46 | echo "正在启动 Flask 应用..." 47 | python3 app.py 48 | if [ $? -ne 0 ]; then 49 | echo "启动 Flask 应用失败。请检查端口是否被占用或配置不正确。" 50 | exit 1 51 | fi 52 | 53 | echo "Flask 应用已启动。默认使用 http://127.0.0.1:9898/v1" 54 | 55 | # 等待 Ctrl+C 退出(在 shell 脚本中通常不需要像 bat 文件那样暂停) 56 | trap "exit" INT TERM # 捕获中断信号(Ctrl+C)和终止信号 57 | wait --------------------------------------------------------------------------------