├── .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
--------------------------------------------------------------------------------