dsh-plugin-image-workbench 0.0.0-stage → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +256 -0
- package/README.md +256 -2
- package/client.js +241 -0
- package/cordis.patch.yml +14 -0
- package/host.js +641 -0
- package/install.mjs +387 -0
- package/package.json +57 -4
- package/scripts/vendor-snapshot.py +154 -0
- package/server/access.py +26 -0
- package/server/app.py +1329 -0
- package/server/catalog.json +1 -0
- package/server/catalog.py +147 -0
- package/server/composer.py +391 -0
- package/server/drafts.py +158 -0
- package/server/history.py +575 -0
- package/server/imaging.py +172 -0
- package/server/pool.py +1335 -0
- package/server/pool_admin.py +921 -0
- package/server/pool_service.py +1073 -0
- package/server/requirements.txt +6 -0
- package/server/smoke_check.py +402 -0
- package/server/static/access.html +202 -0
- package/server/static/index.html +4725 -0
- package/vendor/chatgpt2api/README.md +13 -0
- package/vendor/chatgpt2api/SOURCE.json +11 -0
- package/vendor/chatgpt2api/runtime/uv.exe +0 -0
- package/vendor/chatgpt2api/source/.env.example +82 -0
- package/vendor/chatgpt2api/source/.python-version +1 -0
- package/vendor/chatgpt2api/source/LICENSE +21 -0
- package/vendor/chatgpt2api/source/README.md +391 -0
- package/vendor/chatgpt2api/source/VERSION +1 -0
- package/vendor/chatgpt2api/source/api/__init__.py +2 -0
- package/vendor/chatgpt2api/source/api/accounts.py +537 -0
- package/vendor/chatgpt2api/source/api/ai.py +217 -0
- package/vendor/chatgpt2api/source/api/app.py +62 -0
- package/vendor/chatgpt2api/source/api/errors.py +46 -0
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
- package/vendor/chatgpt2api/source/api/support.py +134 -0
- package/vendor/chatgpt2api/source/api/system.py +365 -0
- package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
- package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
- package/vendor/chatgpt2api/source/config.default.json +108 -0
- package/vendor/chatgpt2api/source/main.py +9 -0
- package/vendor/chatgpt2api/source/pyproject.toml +27 -0
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
- package/vendor/chatgpt2api/source/services/__init__.py +0 -0
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
- package/vendor/chatgpt2api/source/services/config.py +667 -0
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
- package/vendor/chatgpt2api/source/services/image_service.py +376 -0
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
- package/vendor/chatgpt2api/source/services/log_service.py +337 -0
- package/vendor/chatgpt2api/source/services/model_service.py +182 -0
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
- package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
- package/vendor/chatgpt2api/source/utils/helper.py +469 -0
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
- package/vendor/chatgpt2api/source/utils/log.py +110 -0
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
- package/vendor/chatgpt2api/source/utils/pow.py +207 -0
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
- package/vendor/chatgpt2api/source/uv.lock +904 -0
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
- package/vendor/chatgpt2api/source/web/components.json +24 -0
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
- package/vendor/chatgpt2api/source/web/package.json +58 -0
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
- package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
- package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
- package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
DEFAULT_IMAGE_SIZE = (1024, 1024)
|
|
12
|
+
IMAGE_INPUT_TOKEN_MODEL = "gpt-5.4-mini"
|
|
13
|
+
|
|
14
|
+
PATCH_SIZE = 32
|
|
15
|
+
TILE_SIZE = 512
|
|
16
|
+
TILE_HIGH_SHORT_SIDE = 768
|
|
17
|
+
|
|
18
|
+
PATCH_1536_MODELS = (
|
|
19
|
+
"gpt-5.4-mini",
|
|
20
|
+
"gpt-5.4-nano",
|
|
21
|
+
"gpt-5-mini",
|
|
22
|
+
"gpt-5-nano",
|
|
23
|
+
"gpt-5.2",
|
|
24
|
+
"gpt-5.3-codex",
|
|
25
|
+
"gpt-5-codex-mini",
|
|
26
|
+
"gpt-5.1-codex-mini",
|
|
27
|
+
"gpt-5.2-codex",
|
|
28
|
+
"gpt-5.2-chat-latest",
|
|
29
|
+
"o4-mini",
|
|
30
|
+
"gpt-4.1-mini",
|
|
31
|
+
"gpt-4.1-nano",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
PATCH_MULTIPLIERS = {
|
|
35
|
+
"gpt-5.4-mini": 1.62,
|
|
36
|
+
"gpt-5.4-nano": 2.46,
|
|
37
|
+
"gpt-5-mini": 1.62,
|
|
38
|
+
"gpt-5-nano": 2.46,
|
|
39
|
+
"gpt-4.1-mini": 1.62,
|
|
40
|
+
"gpt-4.1-nano": 2.46,
|
|
41
|
+
"o4-mini": 1.72,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _model_name(model: str) -> str:
|
|
46
|
+
return str(model or "").strip().lower()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def image_size_from_bytes(data: bytes) -> tuple[int, int] | None:
|
|
50
|
+
if not data:
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
with Image.open(BytesIO(data)) as image:
|
|
54
|
+
width, height = image.size
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
if width <= 0 or height <= 0:
|
|
58
|
+
return None
|
|
59
|
+
return int(width), int(height)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _decode_data_url(value: str) -> bytes:
|
|
63
|
+
text = str(value or "").strip()
|
|
64
|
+
payload = text.split(",", 1)[1] if text.startswith("data:") and "," in text else text
|
|
65
|
+
return base64.b64decode(payload)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def image_size_from_data_url(value: str) -> tuple[int, int] | None:
|
|
69
|
+
try:
|
|
70
|
+
return image_size_from_bytes(_decode_data_url(value))
|
|
71
|
+
except Exception:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_image_size(size: object, default: tuple[int, int] = DEFAULT_IMAGE_SIZE) -> tuple[int, int]:
|
|
76
|
+
if isinstance(size, (tuple, list)) and len(size) >= 2:
|
|
77
|
+
try:
|
|
78
|
+
width = int(size[0])
|
|
79
|
+
height = int(size[1])
|
|
80
|
+
if width > 0 and height > 0:
|
|
81
|
+
return width, height
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
pass
|
|
84
|
+
match = re.search(r"(\d{2,5})\D+(\d{2,5})", str(size or ""))
|
|
85
|
+
if not match:
|
|
86
|
+
return default
|
|
87
|
+
width, height = int(match.group(1)), int(match.group(2))
|
|
88
|
+
return (width, height) if width > 0 and height > 0 else default
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _patch_count(width: float, height: float) -> int:
|
|
92
|
+
return math.ceil(width / PATCH_SIZE) * math.ceil(height / PATCH_SIZE)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _patch_multiplier(model: str) -> float:
|
|
96
|
+
name = _model_name(model)
|
|
97
|
+
for prefix, multiplier in PATCH_MULTIPLIERS.items():
|
|
98
|
+
if name.startswith(prefix):
|
|
99
|
+
return multiplier
|
|
100
|
+
return 1.0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _patch_limits(model: str, detail: str) -> tuple[int, int] | None:
|
|
104
|
+
name = _model_name(model)
|
|
105
|
+
if any(name.startswith(prefix) for prefix in PATCH_1536_MODELS):
|
|
106
|
+
return 1536, 2048
|
|
107
|
+
if name.startswith("gpt-5.5"):
|
|
108
|
+
return (10000, 6000) if detail in {"auto", "original"} else (2500, 2048)
|
|
109
|
+
if name.startswith("gpt-5.4"):
|
|
110
|
+
return (10000, 6000) if detail == "original" else (2500, 2048)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _patch_tokens(width: int, height: int, model: str, detail: str) -> int:
|
|
115
|
+
multiplier = _patch_multiplier(model)
|
|
116
|
+
if detail == "low":
|
|
117
|
+
return math.ceil(256 * multiplier)
|
|
118
|
+
|
|
119
|
+
limits = _patch_limits(model, detail)
|
|
120
|
+
if limits is None:
|
|
121
|
+
return 0
|
|
122
|
+
patch_budget, max_dimension = limits
|
|
123
|
+
scale = min(1.0, max_dimension / max(width, height))
|
|
124
|
+
resized_width = width * scale
|
|
125
|
+
resized_height = height * scale
|
|
126
|
+
|
|
127
|
+
if _patch_count(resized_width, resized_height) > patch_budget:
|
|
128
|
+
shrink_factor = math.sqrt((PATCH_SIZE * PATCH_SIZE * patch_budget) / (resized_width * resized_height))
|
|
129
|
+
width_units = resized_width * shrink_factor / PATCH_SIZE
|
|
130
|
+
height_units = resized_height * shrink_factor / PATCH_SIZE
|
|
131
|
+
adjusted_shrink_factor = shrink_factor * min(
|
|
132
|
+
math.floor(width_units) / width_units if width_units else 1,
|
|
133
|
+
math.floor(height_units) / height_units if height_units else 1,
|
|
134
|
+
)
|
|
135
|
+
resized_width *= adjusted_shrink_factor
|
|
136
|
+
resized_height *= adjusted_shrink_factor
|
|
137
|
+
|
|
138
|
+
tokens = min(_patch_count(max(1, resized_width), max(1, resized_height)), patch_budget)
|
|
139
|
+
return math.ceil(tokens * multiplier)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _tile_rates(model: str) -> tuple[int, int]:
|
|
143
|
+
name = _model_name(model)
|
|
144
|
+
if name in {"gpt-5", "gpt-5-chat-latest"}:
|
|
145
|
+
return 70, 140
|
|
146
|
+
if name.startswith("gpt-4o-mini"):
|
|
147
|
+
return 2833, 5667
|
|
148
|
+
if name.startswith(("o1", "o1-pro", "o3")):
|
|
149
|
+
return 75, 150
|
|
150
|
+
if name.startswith("computer-use-preview"):
|
|
151
|
+
return 65, 129
|
|
152
|
+
return 85, 170
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _tile_tokens(width: int, height: int, model: str, detail: str) -> int:
|
|
156
|
+
base_tokens, tile_tokens = _tile_rates(model)
|
|
157
|
+
if detail == "low":
|
|
158
|
+
return base_tokens
|
|
159
|
+
|
|
160
|
+
scale = min(1.0, 2048 / max(width, height))
|
|
161
|
+
resized_width = width * scale
|
|
162
|
+
resized_height = height * scale
|
|
163
|
+
short_side = min(resized_width, resized_height)
|
|
164
|
+
if short_side > 0:
|
|
165
|
+
scale = TILE_HIGH_SHORT_SIDE / short_side
|
|
166
|
+
resized_width *= scale
|
|
167
|
+
resized_height *= scale
|
|
168
|
+
|
|
169
|
+
tiles = math.ceil(resized_width / TILE_SIZE) * math.ceil(resized_height / TILE_SIZE)
|
|
170
|
+
return base_tokens + tiles * tile_tokens
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def count_image_input_tokens(
|
|
174
|
+
width: int,
|
|
175
|
+
height: int,
|
|
176
|
+
model: str,
|
|
177
|
+
detail: str = "auto",
|
|
178
|
+
input_fidelity: str = "low",
|
|
179
|
+
) -> int:
|
|
180
|
+
if width <= 0 or height <= 0:
|
|
181
|
+
return 0
|
|
182
|
+
detail = str(detail or "auto").strip().lower() or "auto"
|
|
183
|
+
return _patch_tokens(width, height, IMAGE_INPUT_TOKEN_MODEL, detail)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _part_size(part: dict[str, Any]) -> tuple[int, int] | None:
|
|
187
|
+
try:
|
|
188
|
+
width = int(part.get("width") or 0)
|
|
189
|
+
height = int(part.get("height") or 0)
|
|
190
|
+
except (TypeError, ValueError):
|
|
191
|
+
width = height = 0
|
|
192
|
+
if width > 0 and height > 0:
|
|
193
|
+
return width, height
|
|
194
|
+
|
|
195
|
+
data = part.get("data")
|
|
196
|
+
if isinstance(data, (bytes, bytearray)):
|
|
197
|
+
return image_size_from_bytes(bytes(data))
|
|
198
|
+
|
|
199
|
+
image_url = part.get("image_url")
|
|
200
|
+
if isinstance(image_url, dict):
|
|
201
|
+
image_url = image_url.get("url") or image_url.get("image_url")
|
|
202
|
+
if isinstance(image_url, str) and image_url.startswith("data:"):
|
|
203
|
+
return image_size_from_data_url(image_url)
|
|
204
|
+
|
|
205
|
+
source = part.get("source")
|
|
206
|
+
if isinstance(source, dict) and str(source.get("type") or "") == "base64":
|
|
207
|
+
try:
|
|
208
|
+
return image_size_from_bytes(base64.b64decode(str(source.get("data") or "")))
|
|
209
|
+
except Exception:
|
|
210
|
+
return None
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def count_image_content_tokens(content: object, model: str, default_detail: str = "auto") -> int:
|
|
215
|
+
if not isinstance(content, list):
|
|
216
|
+
return 0
|
|
217
|
+
total = 0
|
|
218
|
+
for part in content:
|
|
219
|
+
if not isinstance(part, dict):
|
|
220
|
+
continue
|
|
221
|
+
part_type = str(part.get("type") or "").strip()
|
|
222
|
+
if part_type not in {"image", "image_url", "input_image"} and not part.get("source"):
|
|
223
|
+
continue
|
|
224
|
+
size = _part_size(part)
|
|
225
|
+
if not size:
|
|
226
|
+
continue
|
|
227
|
+
total += count_image_input_tokens(
|
|
228
|
+
size[0],
|
|
229
|
+
size[1],
|
|
230
|
+
model,
|
|
231
|
+
str(part.get("detail") or default_detail or "auto"),
|
|
232
|
+
str(part.get("input_fidelity") or part.get("inputFidelity") or "low"),
|
|
233
|
+
)
|
|
234
|
+
return total
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def count_image_inputs_tokens(images: object, model: str, default_detail: str = "auto") -> int:
|
|
238
|
+
if not images:
|
|
239
|
+
return 0
|
|
240
|
+
total = 0
|
|
241
|
+
entries = images if isinstance(images, list) else [images]
|
|
242
|
+
for image in entries:
|
|
243
|
+
data = image[0] if isinstance(image, tuple) and image else image
|
|
244
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
245
|
+
continue
|
|
246
|
+
size = image_size_from_bytes(bytes(data))
|
|
247
|
+
if size:
|
|
248
|
+
total += count_image_input_tokens(size[0], size[1], model, default_detail)
|
|
249
|
+
return total
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def count_generated_image_tokens(width: int, height: int, quality: str = "auto") -> int:
|
|
253
|
+
patches = _patch_count(width, height)
|
|
254
|
+
quality = str(quality or "auto").strip().lower()
|
|
255
|
+
if quality == "low":
|
|
256
|
+
return math.ceil(patches * 17 / 64)
|
|
257
|
+
if quality in {"high", "hd"}:
|
|
258
|
+
return math.ceil(patches * 65 / 16)
|
|
259
|
+
return math.ceil(patches * 33 / 32)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def count_image_output_tokens(size: object = None, quality: str = "auto", count: int = 1) -> int:
|
|
263
|
+
width, height = parse_image_size(size)
|
|
264
|
+
return max(0, int(count or 0)) * count_generated_image_tokens(width, height, quality)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def count_image_output_items_tokens(
|
|
268
|
+
items: object,
|
|
269
|
+
size: object = None,
|
|
270
|
+
quality: str = "auto",
|
|
271
|
+
) -> int:
|
|
272
|
+
if not isinstance(items, list) or not items:
|
|
273
|
+
return 0
|
|
274
|
+
fallback_size = parse_image_size(size)
|
|
275
|
+
total = 0
|
|
276
|
+
for item in items:
|
|
277
|
+
image_size = None
|
|
278
|
+
if isinstance(item, dict):
|
|
279
|
+
b64_json = str(item.get("b64_json") or "").strip()
|
|
280
|
+
if b64_json:
|
|
281
|
+
try:
|
|
282
|
+
image_size = image_size_from_bytes(base64.b64decode(b64_json))
|
|
283
|
+
except Exception:
|
|
284
|
+
image_size = None
|
|
285
|
+
width, height = image_size or fallback_size
|
|
286
|
+
total += count_generated_image_tokens(width, height, quality)
|
|
287
|
+
return total
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def token_usage(
|
|
291
|
+
input_text_tokens: int = 0,
|
|
292
|
+
input_image_tokens: int = 0,
|
|
293
|
+
output_text_tokens: int = 0,
|
|
294
|
+
output_image_tokens: int = 0,
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
input_tokens = max(0, int(input_text_tokens or 0)) + max(0, int(input_image_tokens or 0))
|
|
297
|
+
output_tokens = max(0, int(output_text_tokens or 0)) + max(0, int(output_image_tokens or 0))
|
|
298
|
+
return {
|
|
299
|
+
"input_tokens": input_tokens,
|
|
300
|
+
"output_tokens": output_tokens,
|
|
301
|
+
"total_tokens": input_tokens + output_tokens,
|
|
302
|
+
"input_tokens_details": {
|
|
303
|
+
"text_tokens": max(0, int(input_text_tokens or 0)),
|
|
304
|
+
"image_tokens": max(0, int(input_image_tokens or 0)),
|
|
305
|
+
"cached_tokens": 0,
|
|
306
|
+
},
|
|
307
|
+
"output_tokens_details": {
|
|
308
|
+
"text_tokens": max(0, int(output_text_tokens or 0)),
|
|
309
|
+
"image_tokens": max(0, int(output_image_tokens or 0)),
|
|
310
|
+
"reasoning_tokens": 0,
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def image_usage(
|
|
316
|
+
input_text_tokens: int = 0,
|
|
317
|
+
input_image_tokens: int = 0,
|
|
318
|
+
output_tokens: int = 0,
|
|
319
|
+
) -> dict[str, Any]:
|
|
320
|
+
return token_usage(
|
|
321
|
+
input_text_tokens=input_text_tokens,
|
|
322
|
+
input_image_tokens=input_image_tokens,
|
|
323
|
+
output_image_tokens=output_tokens,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def chat_usage_from_image_usage(usage: dict[str, Any]) -> dict[str, Any]:
|
|
328
|
+
input_tokens = int(usage.get("input_tokens") or 0)
|
|
329
|
+
output_tokens = int(usage.get("output_tokens") or 0)
|
|
330
|
+
input_details = usage.get("input_tokens_details") if isinstance(usage.get("input_tokens_details"), dict) else {}
|
|
331
|
+
output_details = usage.get("output_tokens_details") if isinstance(usage.get("output_tokens_details"), dict) else {}
|
|
332
|
+
return {
|
|
333
|
+
"prompt_tokens": input_tokens,
|
|
334
|
+
"completion_tokens": output_tokens,
|
|
335
|
+
"total_tokens": input_tokens + output_tokens,
|
|
336
|
+
"prompt_tokens_details": {
|
|
337
|
+
"text_tokens": int(input_details.get("text_tokens") or 0),
|
|
338
|
+
"image_tokens": int(input_details.get("image_tokens") or 0),
|
|
339
|
+
"cached_tokens": int(input_details.get("cached_tokens") or 0),
|
|
340
|
+
},
|
|
341
|
+
"completion_tokens_details": {
|
|
342
|
+
"text_tokens": int(output_details.get("text_tokens") or 0),
|
|
343
|
+
"image_tokens": int(output_details.get("image_tokens") or 0),
|
|
344
|
+
"reasoning_tokens": int(output_details.get("reasoning_tokens") or 0),
|
|
345
|
+
},
|
|
346
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import binascii
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Logger:
|
|
10
|
+
_DATA_URL_RE = re.compile(r"data:image/[^;]+;base64,[A-Za-z0-9+/=]+")
|
|
11
|
+
_JSON_B64_RE = re.compile(r'("b64_json"\s*:\s*")([A-Za-z0-9+/=]+)(")')
|
|
12
|
+
|
|
13
|
+
def __init__(self, name: str = "chatgpt2api") -> None:
|
|
14
|
+
self._logger = logging.getLogger(name)
|
|
15
|
+
if not self._logger.handlers:
|
|
16
|
+
handler = logging.StreamHandler()
|
|
17
|
+
handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
|
18
|
+
self._logger.addHandler(handler)
|
|
19
|
+
self._logger.setLevel(logging.DEBUG)
|
|
20
|
+
self._logger.propagate = False
|
|
21
|
+
|
|
22
|
+
def _enabled(self, level: str) -> bool:
|
|
23
|
+
try:
|
|
24
|
+
from services.config import config
|
|
25
|
+
levels = set(config.log_levels)
|
|
26
|
+
except Exception:
|
|
27
|
+
levels = set()
|
|
28
|
+
return level in (levels or {"info", "warning", "error"})
|
|
29
|
+
|
|
30
|
+
def _mask_string(self, value: str, keep: int = 10) -> str:
|
|
31
|
+
if len(value) <= keep:
|
|
32
|
+
return value
|
|
33
|
+
return value[:keep] + "..."
|
|
34
|
+
|
|
35
|
+
def _mask_base64(self, value: str) -> str:
|
|
36
|
+
if value.startswith("data:") and ";base64," in value:
|
|
37
|
+
header, _, data = value.partition(",")
|
|
38
|
+
return f"{header},{self._mask_string(data, 24)} (base64 len={len(data)})"
|
|
39
|
+
return f"{self._mask_string(value, 24)} (base64 len={len(value)})"
|
|
40
|
+
|
|
41
|
+
def _is_base64_string(self, value: str) -> bool:
|
|
42
|
+
if len(value) < 64 or len(value) % 4 != 0:
|
|
43
|
+
return False
|
|
44
|
+
if not any(char in value for char in "+/="):
|
|
45
|
+
return False
|
|
46
|
+
try:
|
|
47
|
+
base64.b64decode(value, validate=True)
|
|
48
|
+
return True
|
|
49
|
+
except (binascii.Error, ValueError):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def _sanitize_string(self, value: str) -> str:
|
|
53
|
+
stripped = value.strip()
|
|
54
|
+
if stripped.startswith("data:") and ";base64," in stripped:
|
|
55
|
+
return self._mask_base64(stripped)
|
|
56
|
+
if self._is_base64_string(stripped):
|
|
57
|
+
return self._mask_base64(stripped)
|
|
58
|
+
sanitized = self._DATA_URL_RE.sub(lambda match: self._mask_base64(match.group(0)), value)
|
|
59
|
+
sanitized = self._JSON_B64_RE.sub(
|
|
60
|
+
lambda match: f'{match.group(1)}{self._mask_base64(match.group(2))}{match.group(3)}',
|
|
61
|
+
sanitized,
|
|
62
|
+
)
|
|
63
|
+
if sanitized != value:
|
|
64
|
+
return sanitized
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
def _sanitize(self, value: Any) -> Any:
|
|
68
|
+
if isinstance(value, dict):
|
|
69
|
+
sanitized = {}
|
|
70
|
+
for key, item in value.items():
|
|
71
|
+
lowered_key = key.lower()
|
|
72
|
+
if isinstance(item, str) and ("token" in lowered_key or lowered_key == "dx"):
|
|
73
|
+
sanitized[key] = self._mask_string(item)
|
|
74
|
+
elif isinstance(item, str) and ("base64" in lowered_key or lowered_key == "b64_json"):
|
|
75
|
+
sanitized[key] = self._mask_base64(item)
|
|
76
|
+
else:
|
|
77
|
+
sanitized[key] = self._sanitize(item)
|
|
78
|
+
return sanitized
|
|
79
|
+
if isinstance(value, list):
|
|
80
|
+
return [self._sanitize(item) for item in value]
|
|
81
|
+
if isinstance(value, tuple):
|
|
82
|
+
return tuple(self._sanitize(item) for item in value)
|
|
83
|
+
if isinstance(value, str):
|
|
84
|
+
return self._sanitize_string(value)
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
def _message(self, value: Any) -> str:
|
|
88
|
+
sanitized = self._sanitize(value)
|
|
89
|
+
if isinstance(sanitized, str):
|
|
90
|
+
return sanitized
|
|
91
|
+
return json.dumps(sanitized, ensure_ascii=False, default=str)
|
|
92
|
+
|
|
93
|
+
def debug(self, message: Any) -> None:
|
|
94
|
+
if self._enabled("debug"):
|
|
95
|
+
self._logger.debug(self._message(message))
|
|
96
|
+
|
|
97
|
+
def info(self, message: Any) -> None:
|
|
98
|
+
if self._enabled("info"):
|
|
99
|
+
self._logger.info(self._message(message))
|
|
100
|
+
|
|
101
|
+
def warning(self, message: Any) -> None:
|
|
102
|
+
if self._enabled("warning"):
|
|
103
|
+
self._logger.warning(self._message(message))
|
|
104
|
+
|
|
105
|
+
def error(self, message: Any) -> None:
|
|
106
|
+
if self._enabled("error"):
|
|
107
|
+
self._logger.error(self._message(message))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
logger = Logger()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""PKCE (Proof Key for Code Exchange) 工具函数"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import secrets
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_pkce() -> tuple[str, str]:
|
|
10
|
+
"""生成 PKCE code_verifier 与对应的 code_challenge(S256)。
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
(code_verifier, code_challenge) 元组
|
|
14
|
+
"""
|
|
15
|
+
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(64)).rstrip(b"=").decode("ascii")
|
|
16
|
+
code_challenge = base64.urlsafe_b64encode(
|
|
17
|
+
hashlib.sha256(code_verifier.encode("ascii")).digest()
|
|
18
|
+
).rstrip(b"=").decode("ascii")
|
|
19
|
+
return code_verifier, code_challenge
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from html.parser import HTMLParser
|
|
8
|
+
from typing import Any, Sequence
|
|
9
|
+
|
|
10
|
+
import pybase64
|
|
11
|
+
|
|
12
|
+
DEFAULT_POW_SCRIPT = "https://chatgpt.com/backend-api/sentinel/sdk.js"
|
|
13
|
+
from utils.helper import new_uuid
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
CORES = [8, 16, 24, 32]
|
|
17
|
+
DOCUMENT_KEYS = ["__reactContainer$fzelfjyxej8", "_reactListening5dehydibo78", "location"]
|
|
18
|
+
SCREEN_RESOLUTIONS = [[1920, 1080], [1440, 900], [2560, 1440], [3840, 2160]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ScriptSrcParser(HTMLParser):
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.script_sources: list[str] = []
|
|
25
|
+
self.data_build = ""
|
|
26
|
+
|
|
27
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
28
|
+
if tag != "script":
|
|
29
|
+
return
|
|
30
|
+
attrs_dict = dict(attrs)
|
|
31
|
+
src = attrs_dict.get("src")
|
|
32
|
+
if not src:
|
|
33
|
+
return
|
|
34
|
+
self.script_sources.append(src)
|
|
35
|
+
match = re.search(r"c/[^/]*/_", src)
|
|
36
|
+
if match:
|
|
37
|
+
self.data_build = match.group(0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_pow_resources(html_content: str) -> tuple[list[str], str]:
|
|
41
|
+
parser = ScriptSrcParser()
|
|
42
|
+
parser.feed(html_content)
|
|
43
|
+
script_sources = parser.script_sources or [DEFAULT_POW_SCRIPT]
|
|
44
|
+
data_build = parser.data_build
|
|
45
|
+
if not data_build:
|
|
46
|
+
match = re.search(r'<html[^>]*data-build="([^"]*)"', html_content)
|
|
47
|
+
if match:
|
|
48
|
+
data_build = match.group(1)
|
|
49
|
+
return script_sources, data_build
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _legacy_parse_time() -> str:
|
|
53
|
+
now = datetime.now(timezone(timedelta(hours=-5)))
|
|
54
|
+
return now.strftime("%a %b %d %Y %H:%M:%S") + " GMT-0500 (Eastern Standard Time)"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_pow_config(
|
|
58
|
+
user_agent: str,
|
|
59
|
+
script_sources: Sequence[str] | None = None,
|
|
60
|
+
data_build: str = "",
|
|
61
|
+
) -> list[Any]:
|
|
62
|
+
navigator_key = random.choice([
|
|
63
|
+
"registerProtocolHandler−function registerProtocolHandler() { [native code] }",
|
|
64
|
+
"storage−[object StorageManager]",
|
|
65
|
+
"locks−[object LockManager]",
|
|
66
|
+
"appCodeName−Mozilla",
|
|
67
|
+
"permissions−[object Permissions]",
|
|
68
|
+
"share−function share() { [native code] }",
|
|
69
|
+
"webdriver−false",
|
|
70
|
+
"managed−[object NavigatorManagedData]",
|
|
71
|
+
"canShare−function canShare() { [native code] }",
|
|
72
|
+
"vendor−Google Inc.",
|
|
73
|
+
"mediaDevices−[object MediaDevices]",
|
|
74
|
+
"vibrate−function vibrate() { [native code] }",
|
|
75
|
+
"storageBuckets−[object StorageBucketManager]",
|
|
76
|
+
"mediaCapabilities−[object MediaCapabilities]",
|
|
77
|
+
"cookieEnabled−true",
|
|
78
|
+
"virtualKeyboard−[object VirtualKeyboard]",
|
|
79
|
+
"product−Gecko",
|
|
80
|
+
"presentation−[object Presentation]",
|
|
81
|
+
"onLine−true",
|
|
82
|
+
"mimeTypes−[object MimeTypeArray]",
|
|
83
|
+
"credentials−[object CredentialsContainer]",
|
|
84
|
+
"serviceWorker−[object ServiceWorkerContainer]",
|
|
85
|
+
"keyboard−[object Keyboard]",
|
|
86
|
+
"gpu−[object GPU]",
|
|
87
|
+
"doNotTrack",
|
|
88
|
+
"serial−[object Serial]",
|
|
89
|
+
"pdfViewerEnabled−true",
|
|
90
|
+
"language−zh-CN",
|
|
91
|
+
"geolocation−[object Geolocation]",
|
|
92
|
+
"userAgentData−[object NavigatorUAData]",
|
|
93
|
+
"getUserMedia−function getUserMedia() { [native code] }",
|
|
94
|
+
"sendBeacon−function sendBeacon() { [native code] }",
|
|
95
|
+
"hardwareConcurrency−32",
|
|
96
|
+
"windowControlsOverlay−[object WindowControlsOverlay]",
|
|
97
|
+
])
|
|
98
|
+
window_key = random.choice([
|
|
99
|
+
"0",
|
|
100
|
+
"window",
|
|
101
|
+
"self",
|
|
102
|
+
"document",
|
|
103
|
+
"name",
|
|
104
|
+
"location",
|
|
105
|
+
"customElements",
|
|
106
|
+
"history",
|
|
107
|
+
"navigation",
|
|
108
|
+
"innerWidth",
|
|
109
|
+
"innerHeight",
|
|
110
|
+
"scrollX",
|
|
111
|
+
"scrollY",
|
|
112
|
+
"visualViewport",
|
|
113
|
+
"screenX",
|
|
114
|
+
"screenY",
|
|
115
|
+
"outerWidth",
|
|
116
|
+
"outerHeight",
|
|
117
|
+
"devicePixelRatio",
|
|
118
|
+
"screen",
|
|
119
|
+
"chrome",
|
|
120
|
+
"navigator",
|
|
121
|
+
"onresize",
|
|
122
|
+
"performance",
|
|
123
|
+
"crypto",
|
|
124
|
+
"indexedDB",
|
|
125
|
+
"sessionStorage",
|
|
126
|
+
"localStorage",
|
|
127
|
+
"scheduler",
|
|
128
|
+
"alert",
|
|
129
|
+
"atob",
|
|
130
|
+
"btoa",
|
|
131
|
+
"fetch",
|
|
132
|
+
"matchMedia",
|
|
133
|
+
"postMessage",
|
|
134
|
+
"queueMicrotask",
|
|
135
|
+
"requestAnimationFrame",
|
|
136
|
+
"setInterval",
|
|
137
|
+
"setTimeout",
|
|
138
|
+
"caches",
|
|
139
|
+
"__NEXT_DATA__",
|
|
140
|
+
"__BUILD_MANIFEST",
|
|
141
|
+
"__NEXT_PRELOADREADY",
|
|
142
|
+
])
|
|
143
|
+
script_source = random.choice(list(script_sources)) if script_sources else DEFAULT_POW_SCRIPT
|
|
144
|
+
return [
|
|
145
|
+
sum(random.choices(SCREEN_RESOLUTIONS, k=1)[0]),
|
|
146
|
+
_legacy_parse_time(),
|
|
147
|
+
4294705152,
|
|
148
|
+
1,
|
|
149
|
+
user_agent,
|
|
150
|
+
script_source,
|
|
151
|
+
data_build,
|
|
152
|
+
"en-US",
|
|
153
|
+
"en-US,es-US,en,es",
|
|
154
|
+
random.random(),
|
|
155
|
+
navigator_key,
|
|
156
|
+
random.choice(DOCUMENT_KEYS),
|
|
157
|
+
window_key,
|
|
158
|
+
time.perf_counter() * 1000,
|
|
159
|
+
new_uuid(),
|
|
160
|
+
"",
|
|
161
|
+
random.choice(CORES),
|
|
162
|
+
time.time() * 1000 - (time.perf_counter() * 1000),
|
|
163
|
+
0, 0, 0, 0, 0, 0,
|
|
164
|
+
0, # 0 = edge/chrome, 1 = firefox
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _pow_generate(seed: str, difficulty: str, config: list[Any], limit: int = 500000) -> tuple[str, bool]:
|
|
169
|
+
target = bytes.fromhex(difficulty)
|
|
170
|
+
diff_len = len(difficulty) // 2
|
|
171
|
+
seed_bytes = seed.encode()
|
|
172
|
+
static_1 = (json.dumps(config[:3], separators=(",", ":"), ensure_ascii=False)[:-1] + ",").encode()
|
|
173
|
+
static_2 = ("," + json.dumps(config[4:9], separators=(",", ":"), ensure_ascii=False)[1:-1] + ",").encode()
|
|
174
|
+
static_3 = ("," + json.dumps(config[10:], separators=(",", ":"), ensure_ascii=False)[1:]).encode()
|
|
175
|
+
for i in range(limit):
|
|
176
|
+
final_json = static_1 + str(i).encode() + static_2 + str(i >> 1).encode() + static_3
|
|
177
|
+
encoded = pybase64.b64encode(final_json)
|
|
178
|
+
digest = hashlib.sha3_512(seed_bytes + encoded).digest()
|
|
179
|
+
if digest[:diff_len] <= target:
|
|
180
|
+
return encoded.decode(), True
|
|
181
|
+
fallback = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + pybase64.b64encode(f'"{seed}"'.encode()).decode()
|
|
182
|
+
return fallback, False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_legacy_requirements_token(
|
|
186
|
+
user_agent: str,
|
|
187
|
+
script_sources: Sequence[str] | None = None,
|
|
188
|
+
data_build: str = "",
|
|
189
|
+
) -> str:
|
|
190
|
+
config = build_pow_config(user_agent, script_sources=script_sources, data_build=data_build)
|
|
191
|
+
return "gAAAAAC" + pybase64.b64encode(
|
|
192
|
+
json.dumps(config, separators=(",", ":"), ensure_ascii=False).encode()
|
|
193
|
+
).decode()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_proof_token(
|
|
197
|
+
seed: str,
|
|
198
|
+
difficulty: str,
|
|
199
|
+
user_agent: str,
|
|
200
|
+
script_sources: Sequence[str] | None = None,
|
|
201
|
+
data_build: str = "",
|
|
202
|
+
) -> str:
|
|
203
|
+
config = build_pow_config(user_agent, script_sources=script_sources, data_build=data_build)
|
|
204
|
+
answer, solved = _pow_generate(seed, difficulty, config)
|
|
205
|
+
if not solved:
|
|
206
|
+
raise RuntimeError(f"failed to solve proof token: difficulty={difficulty}")
|
|
207
|
+
return "gAAAAAB" + answer
|