dsh-plugin-image-workbench 1.1.5 → 1.1.7
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 -21
- package/README.en.md +5 -3
- package/README.md +7 -3
- package/client.js +241 -241
- package/cordis.patch.yml +14 -14
- package/host.js +51 -0
- package/install.mjs +333 -387
- package/package.json +3 -2
- package/scripts/fetch-style-images.mjs +104 -0
- package/scripts/vendor-snapshot.py +154 -154
- package/server/access.py +26 -26
- package/server/app.py +1371 -1329
- package/server/catalog.py +147 -147
- package/server/composer.py +391 -391
- package/server/drafts.py +158 -158
- package/server/history.py +575 -575
- package/server/imaging.py +172 -172
- package/server/pool.py +1335 -1335
- package/server/pool_admin.py +921 -921
- package/server/pool_service.py +1073 -1073
- package/server/requirements.txt +6 -6
- package/server/smoke_check.py +402 -402
- package/server/static/access.html +202 -202
- package/server/static/index.html +15 -0
- package/vendor/chatgpt2api/source/.env.example +82 -82
- package/vendor/chatgpt2api/source/.python-version +1 -1
- package/vendor/chatgpt2api/source/LICENSE +21 -21
- package/vendor/chatgpt2api/source/README.md +391 -391
- package/vendor/chatgpt2api/source/VERSION +1 -1
- package/vendor/chatgpt2api/source/api/__init__.py +2 -2
- package/vendor/chatgpt2api/source/api/accounts.py +537 -537
- package/vendor/chatgpt2api/source/api/ai.py +217 -217
- package/vendor/chatgpt2api/source/api/app.py +62 -62
- package/vendor/chatgpt2api/source/api/errors.py +46 -46
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
- package/vendor/chatgpt2api/source/api/support.py +134 -134
- package/vendor/chatgpt2api/source/api/system.py +365 -365
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
- package/vendor/chatgpt2api/source/config.default.json +108 -108
- package/vendor/chatgpt2api/source/main.py +9 -9
- package/vendor/chatgpt2api/source/pyproject.toml +27 -27
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
- package/vendor/chatgpt2api/source/services/config.py +667 -667
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
- package/vendor/chatgpt2api/source/services/image_service.py +376 -376
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
- package/vendor/chatgpt2api/source/services/log_service.py +337 -337
- package/vendor/chatgpt2api/source/services/model_service.py +182 -182
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
- package/vendor/chatgpt2api/source/utils/helper.py +469 -469
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
- package/vendor/chatgpt2api/source/utils/log.py +110 -110
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
- package/vendor/chatgpt2api/source/utils/pow.py +207 -207
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
- package/vendor/chatgpt2api/source/uv.lock +904 -904
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
- package/vendor/chatgpt2api/source/web/components.json +24 -24
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
- package/vendor/chatgpt2api/source/web/package.json +58 -58
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
|
@@ -1,315 +1,315 @@
|
|
|
1
|
-
"""CLIProxyAPI integration for browsing remote auth files and importing selected tokens."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import threading
|
|
7
|
-
import uuid
|
|
8
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
-
from datetime import datetime, timezone
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from threading import Lock
|
|
12
|
-
|
|
13
|
-
from curl_cffi.requests import Session
|
|
14
|
-
|
|
15
|
-
from services.account_service import account_service
|
|
16
|
-
from services.config import DATA_DIR
|
|
17
|
-
from services.proxy_service import proxy_settings
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
CPA_CONFIG_FILE = DATA_DIR / "cpa_config.json"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _new_id() -> str:
|
|
24
|
-
return uuid.uuid4().hex[:12]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _now_iso() -> str:
|
|
28
|
-
return datetime.now(timezone.utc).isoformat()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _normalize_import_job(raw: object, *, fail_unfinished: bool) -> dict | None:
|
|
32
|
-
if not isinstance(raw, dict):
|
|
33
|
-
return None
|
|
34
|
-
status = str(raw.get("status") or "failed").strip() or "failed"
|
|
35
|
-
if fail_unfinished and status in {"pending", "running"}:
|
|
36
|
-
status = "failed"
|
|
37
|
-
return {
|
|
38
|
-
"job_id": str(raw.get("job_id") or uuid.uuid4().hex).strip(),
|
|
39
|
-
"status": status,
|
|
40
|
-
"created_at": str(raw.get("created_at") or _now_iso()).strip() or _now_iso(),
|
|
41
|
-
"updated_at": str(raw.get("updated_at") or raw.get("created_at") or _now_iso()).strip() or _now_iso(),
|
|
42
|
-
"total": int(raw.get("total") or 0),
|
|
43
|
-
"completed": int(raw.get("completed") or 0),
|
|
44
|
-
"added": int(raw.get("added") or 0),
|
|
45
|
-
"skipped": int(raw.get("skipped") or 0),
|
|
46
|
-
"refreshed": int(raw.get("refreshed") or 0),
|
|
47
|
-
"failed": int(raw.get("failed") or 0),
|
|
48
|
-
"errors": raw.get("errors") if isinstance(raw.get("errors"), list) else [],
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _normalize_pool(raw: dict) -> dict:
|
|
53
|
-
return {
|
|
54
|
-
"id": str(raw.get("id") or _new_id()).strip(),
|
|
55
|
-
"name": str(raw.get("name") or "").strip(),
|
|
56
|
-
"base_url": str(raw.get("base_url") or "").strip(),
|
|
57
|
-
"secret_key": str(raw.get("secret_key") or "").strip(),
|
|
58
|
-
"import_job": _normalize_import_job(raw.get("import_job"), fail_unfinished=True),
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _management_headers(secret_key: str) -> dict[str, str]:
|
|
63
|
-
return {
|
|
64
|
-
"Authorization": f"Bearer {secret_key}",
|
|
65
|
-
"Accept": "application/json",
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class CPAConfig:
|
|
70
|
-
def __init__(self, store_file: Path):
|
|
71
|
-
self._store_file = store_file
|
|
72
|
-
self._lock = Lock()
|
|
73
|
-
self._pools: list[dict] = self._load()
|
|
74
|
-
|
|
75
|
-
def _load(self) -> list[dict]:
|
|
76
|
-
if not self._store_file.exists():
|
|
77
|
-
return []
|
|
78
|
-
try:
|
|
79
|
-
raw = json.loads(self._store_file.read_text(encoding="utf-8"))
|
|
80
|
-
if isinstance(raw, dict) and "base_url" in raw:
|
|
81
|
-
pool = _normalize_pool(raw)
|
|
82
|
-
return [pool] if pool["base_url"] else []
|
|
83
|
-
if isinstance(raw, list):
|
|
84
|
-
return [_normalize_pool(item) for item in raw if isinstance(item, dict)]
|
|
85
|
-
except Exception:
|
|
86
|
-
pass
|
|
87
|
-
return []
|
|
88
|
-
|
|
89
|
-
def _save(self) -> None:
|
|
90
|
-
self._store_file.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
-
self._store_file.write_text(json.dumps(self._pools, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
92
|
-
|
|
93
|
-
def list_pools(self) -> list[dict]:
|
|
94
|
-
with self._lock:
|
|
95
|
-
return [dict(pool) for pool in self._pools]
|
|
96
|
-
|
|
97
|
-
def get_pool(self, pool_id: str) -> dict | None:
|
|
98
|
-
with self._lock:
|
|
99
|
-
for pool in self._pools:
|
|
100
|
-
if pool["id"] == pool_id:
|
|
101
|
-
return dict(pool)
|
|
102
|
-
return None
|
|
103
|
-
|
|
104
|
-
def add_pool(self, name: str, base_url: str, secret_key: str) -> dict:
|
|
105
|
-
pool = _normalize_pool({"id": _new_id(), "name": name, "base_url": base_url, "secret_key": secret_key})
|
|
106
|
-
with self._lock:
|
|
107
|
-
self._pools.append(pool)
|
|
108
|
-
self._save()
|
|
109
|
-
return dict(pool)
|
|
110
|
-
|
|
111
|
-
def update_pool(self, pool_id: str, updates: dict) -> dict | None:
|
|
112
|
-
with self._lock:
|
|
113
|
-
for index, pool in enumerate(self._pools):
|
|
114
|
-
if pool["id"] != pool_id:
|
|
115
|
-
continue
|
|
116
|
-
merged = {**pool, **{key: value for key, value in updates.items() if value is not None}, "id": pool_id}
|
|
117
|
-
self._pools[index] = _normalize_pool(merged)
|
|
118
|
-
self._save()
|
|
119
|
-
return dict(self._pools[index])
|
|
120
|
-
return None
|
|
121
|
-
|
|
122
|
-
def delete_pool(self, pool_id: str) -> bool:
|
|
123
|
-
with self._lock:
|
|
124
|
-
before = len(self._pools)
|
|
125
|
-
self._pools = [pool for pool in self._pools if pool["id"] != pool_id]
|
|
126
|
-
if len(self._pools) < before:
|
|
127
|
-
self._save()
|
|
128
|
-
return True
|
|
129
|
-
return False
|
|
130
|
-
|
|
131
|
-
def set_import_job(self, pool_id: str, import_job: dict | None) -> dict | None:
|
|
132
|
-
with self._lock:
|
|
133
|
-
for index, pool in enumerate(self._pools):
|
|
134
|
-
if pool["id"] != pool_id:
|
|
135
|
-
continue
|
|
136
|
-
next_pool = dict(pool)
|
|
137
|
-
next_pool["import_job"] = _normalize_import_job(import_job, fail_unfinished=False)
|
|
138
|
-
self._pools[index] = next_pool
|
|
139
|
-
self._save()
|
|
140
|
-
return dict(next_pool)
|
|
141
|
-
return None
|
|
142
|
-
|
|
143
|
-
def get_import_job(self, pool_id: str) -> dict | None:
|
|
144
|
-
with self._lock:
|
|
145
|
-
for pool in self._pools:
|
|
146
|
-
if pool["id"] == pool_id:
|
|
147
|
-
job = pool.get("import_job")
|
|
148
|
-
return dict(job) if isinstance(job, dict) else None
|
|
149
|
-
return None
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def list_remote_files(pool: dict) -> list[dict]:
|
|
153
|
-
base_url = str(pool.get("base_url") or "").strip()
|
|
154
|
-
secret_key = str(pool.get("secret_key") or "").strip()
|
|
155
|
-
if not base_url or not secret_key:
|
|
156
|
-
return []
|
|
157
|
-
|
|
158
|
-
url = f"{base_url.rstrip('/')}/v0/management/auth-files"
|
|
159
|
-
session = Session(**proxy_settings.build_session_kwargs(verify=True))
|
|
160
|
-
try:
|
|
161
|
-
response = session.get(url, headers=_management_headers(secret_key), timeout=30)
|
|
162
|
-
if not response.ok:
|
|
163
|
-
raise RuntimeError(f"remote list failed: HTTP {response.status_code}")
|
|
164
|
-
payload = response.json()
|
|
165
|
-
finally:
|
|
166
|
-
session.close()
|
|
167
|
-
|
|
168
|
-
files = payload.get("files") if isinstance(payload, dict) else None
|
|
169
|
-
if not isinstance(files, list):
|
|
170
|
-
raise RuntimeError("remote list payload is invalid")
|
|
171
|
-
|
|
172
|
-
items: list[dict] = []
|
|
173
|
-
for item in files:
|
|
174
|
-
if not isinstance(item, dict):
|
|
175
|
-
continue
|
|
176
|
-
name = str(item.get("name") or "").strip()
|
|
177
|
-
email = str(item.get("email") or item.get("account") or "").strip()
|
|
178
|
-
if not name:
|
|
179
|
-
continue
|
|
180
|
-
items.append({"name": name, "email": email})
|
|
181
|
-
return items
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def fetch_remote_access_token(pool: dict, file_name: str) -> tuple[str | None, str | None]:
|
|
185
|
-
base_url = str(pool.get("base_url") or "").strip()
|
|
186
|
-
secret_key = str(pool.get("secret_key") or "").strip()
|
|
187
|
-
file_name = str(file_name or "").strip()
|
|
188
|
-
if not base_url or not secret_key or not file_name:
|
|
189
|
-
return None, "invalid request"
|
|
190
|
-
|
|
191
|
-
url = f"{base_url.rstrip('/')}/v0/management/auth-files/download"
|
|
192
|
-
session = Session(**proxy_settings.build_session_kwargs(verify=True))
|
|
193
|
-
try:
|
|
194
|
-
response = session.get(url, headers=_management_headers(secret_key), params={"name": file_name}, timeout=30)
|
|
195
|
-
if not response.ok:
|
|
196
|
-
return None, f"HTTP {response.status_code}"
|
|
197
|
-
payload = response.json()
|
|
198
|
-
except Exception as exc:
|
|
199
|
-
return None, str(exc)
|
|
200
|
-
finally:
|
|
201
|
-
session.close()
|
|
202
|
-
|
|
203
|
-
if not isinstance(payload, dict):
|
|
204
|
-
return None, "invalid payload"
|
|
205
|
-
|
|
206
|
-
access_token = str(payload.get("access_token") or "").strip()
|
|
207
|
-
if not access_token:
|
|
208
|
-
return None, "missing access_token"
|
|
209
|
-
return access_token, None
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
class CPAImportService:
|
|
213
|
-
def __init__(self, cpa_config: CPAConfig):
|
|
214
|
-
self._config = cpa_config
|
|
215
|
-
|
|
216
|
-
def start_import(self, pool: dict, selected_files: list[str]) -> dict:
|
|
217
|
-
names = [str(name or "").strip() for name in selected_files if str(name or "").strip()]
|
|
218
|
-
if not names:
|
|
219
|
-
raise ValueError("selected files is required")
|
|
220
|
-
|
|
221
|
-
pool_id = str(pool.get("id") or "").strip()
|
|
222
|
-
job = {
|
|
223
|
-
"job_id": uuid.uuid4().hex,
|
|
224
|
-
"status": "pending",
|
|
225
|
-
"created_at": _now_iso(),
|
|
226
|
-
"updated_at": _now_iso(),
|
|
227
|
-
"total": len(names),
|
|
228
|
-
"completed": 0,
|
|
229
|
-
"added": 0,
|
|
230
|
-
"skipped": 0,
|
|
231
|
-
"refreshed": 0,
|
|
232
|
-
"failed": 0,
|
|
233
|
-
"errors": [],
|
|
234
|
-
}
|
|
235
|
-
saved_pool = self._config.set_import_job(pool_id, job)
|
|
236
|
-
if saved_pool is None:
|
|
237
|
-
raise ValueError("pool not found")
|
|
238
|
-
|
|
239
|
-
thread = threading.Thread(
|
|
240
|
-
target=self._run_import,
|
|
241
|
-
args=(pool_id, pool, names),
|
|
242
|
-
name=f"cpa-import-{pool_id}",
|
|
243
|
-
daemon=True,
|
|
244
|
-
)
|
|
245
|
-
thread.start()
|
|
246
|
-
return dict(saved_pool.get("import_job") or job)
|
|
247
|
-
|
|
248
|
-
def _update_job(self, pool_id: str, **updates) -> dict | None:
|
|
249
|
-
current = self._config.get_import_job(pool_id)
|
|
250
|
-
if current is None:
|
|
251
|
-
return None
|
|
252
|
-
next_job = {**current, **updates, "updated_at": _now_iso()}
|
|
253
|
-
pool = self._config.set_import_job(pool_id, next_job)
|
|
254
|
-
if pool is None:
|
|
255
|
-
return None
|
|
256
|
-
job = pool.get("import_job")
|
|
257
|
-
return dict(job) if isinstance(job, dict) else None
|
|
258
|
-
|
|
259
|
-
def _append_error(self, pool_id: str, file_name: str, message: str) -> None:
|
|
260
|
-
current = self._config.get_import_job(pool_id)
|
|
261
|
-
if current is None:
|
|
262
|
-
return
|
|
263
|
-
errors = list(current.get("errors") or [])
|
|
264
|
-
errors.append({"name": file_name, "error": message})
|
|
265
|
-
self._update_job(pool_id, errors=errors, failed=len(errors))
|
|
266
|
-
|
|
267
|
-
def _run_import(self, pool_id: str, pool: dict, names: list[str]) -> None:
|
|
268
|
-
self._update_job(pool_id, status="running")
|
|
269
|
-
|
|
270
|
-
tokens: list[str] = []
|
|
271
|
-
max_workers = min(16, max(1, len(names)))
|
|
272
|
-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
273
|
-
future_map = {executor.submit(fetch_remote_access_token, pool, name): name for name in names}
|
|
274
|
-
for future in as_completed(future_map):
|
|
275
|
-
file_name = future_map[future]
|
|
276
|
-
try:
|
|
277
|
-
token, error = future.result()
|
|
278
|
-
except Exception as exc:
|
|
279
|
-
token, error = None, str(exc)
|
|
280
|
-
|
|
281
|
-
if token:
|
|
282
|
-
tokens.append(token)
|
|
283
|
-
else:
|
|
284
|
-
self._append_error(pool_id, file_name, error or "unknown error")
|
|
285
|
-
|
|
286
|
-
current = self._config.get_import_job(pool_id) or {}
|
|
287
|
-
failed = len(current.get("errors") or [])
|
|
288
|
-
self._update_job(pool_id, completed=int(current.get("completed") or 0) + 1, failed=failed)
|
|
289
|
-
|
|
290
|
-
if not tokens:
|
|
291
|
-
current = self._config.get_import_job(pool_id) or {}
|
|
292
|
-
self._update_job(
|
|
293
|
-
pool_id,
|
|
294
|
-
status="failed",
|
|
295
|
-
completed=int(current.get("total") or 0),
|
|
296
|
-
failed=len(current.get("errors") or []),
|
|
297
|
-
)
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
add_result = account_service.add_accounts(tokens, source_type="codex")
|
|
301
|
-
refresh_result = account_service.refresh_accounts(tokens)
|
|
302
|
-
current = self._config.get_import_job(pool_id) or {}
|
|
303
|
-
self._update_job(
|
|
304
|
-
pool_id,
|
|
305
|
-
status="completed",
|
|
306
|
-
completed=len(names),
|
|
307
|
-
added=int(add_result.get("added") or 0),
|
|
308
|
-
skipped=int(add_result.get("skipped") or 0),
|
|
309
|
-
refreshed=int(refresh_result.get("refreshed") or 0),
|
|
310
|
-
failed=len(current.get("errors") or []),
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
cpa_config = CPAConfig(CPA_CONFIG_FILE)
|
|
315
|
-
cpa_import_service = CPAImportService(cpa_config)
|
|
1
|
+
"""CLIProxyAPI integration for browsing remote auth files and importing selected tokens."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
import uuid
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from threading import Lock
|
|
12
|
+
|
|
13
|
+
from curl_cffi.requests import Session
|
|
14
|
+
|
|
15
|
+
from services.account_service import account_service
|
|
16
|
+
from services.config import DATA_DIR
|
|
17
|
+
from services.proxy_service import proxy_settings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CPA_CONFIG_FILE = DATA_DIR / "cpa_config.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _new_id() -> str:
|
|
24
|
+
return uuid.uuid4().hex[:12]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _now_iso() -> str:
|
|
28
|
+
return datetime.now(timezone.utc).isoformat()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_import_job(raw: object, *, fail_unfinished: bool) -> dict | None:
|
|
32
|
+
if not isinstance(raw, dict):
|
|
33
|
+
return None
|
|
34
|
+
status = str(raw.get("status") or "failed").strip() or "failed"
|
|
35
|
+
if fail_unfinished and status in {"pending", "running"}:
|
|
36
|
+
status = "failed"
|
|
37
|
+
return {
|
|
38
|
+
"job_id": str(raw.get("job_id") or uuid.uuid4().hex).strip(),
|
|
39
|
+
"status": status,
|
|
40
|
+
"created_at": str(raw.get("created_at") or _now_iso()).strip() or _now_iso(),
|
|
41
|
+
"updated_at": str(raw.get("updated_at") or raw.get("created_at") or _now_iso()).strip() or _now_iso(),
|
|
42
|
+
"total": int(raw.get("total") or 0),
|
|
43
|
+
"completed": int(raw.get("completed") or 0),
|
|
44
|
+
"added": int(raw.get("added") or 0),
|
|
45
|
+
"skipped": int(raw.get("skipped") or 0),
|
|
46
|
+
"refreshed": int(raw.get("refreshed") or 0),
|
|
47
|
+
"failed": int(raw.get("failed") or 0),
|
|
48
|
+
"errors": raw.get("errors") if isinstance(raw.get("errors"), list) else [],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _normalize_pool(raw: dict) -> dict:
|
|
53
|
+
return {
|
|
54
|
+
"id": str(raw.get("id") or _new_id()).strip(),
|
|
55
|
+
"name": str(raw.get("name") or "").strip(),
|
|
56
|
+
"base_url": str(raw.get("base_url") or "").strip(),
|
|
57
|
+
"secret_key": str(raw.get("secret_key") or "").strip(),
|
|
58
|
+
"import_job": _normalize_import_job(raw.get("import_job"), fail_unfinished=True),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _management_headers(secret_key: str) -> dict[str, str]:
|
|
63
|
+
return {
|
|
64
|
+
"Authorization": f"Bearer {secret_key}",
|
|
65
|
+
"Accept": "application/json",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CPAConfig:
|
|
70
|
+
def __init__(self, store_file: Path):
|
|
71
|
+
self._store_file = store_file
|
|
72
|
+
self._lock = Lock()
|
|
73
|
+
self._pools: list[dict] = self._load()
|
|
74
|
+
|
|
75
|
+
def _load(self) -> list[dict]:
|
|
76
|
+
if not self._store_file.exists():
|
|
77
|
+
return []
|
|
78
|
+
try:
|
|
79
|
+
raw = json.loads(self._store_file.read_text(encoding="utf-8"))
|
|
80
|
+
if isinstance(raw, dict) and "base_url" in raw:
|
|
81
|
+
pool = _normalize_pool(raw)
|
|
82
|
+
return [pool] if pool["base_url"] else []
|
|
83
|
+
if isinstance(raw, list):
|
|
84
|
+
return [_normalize_pool(item) for item in raw if isinstance(item, dict)]
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
def _save(self) -> None:
|
|
90
|
+
self._store_file.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
self._store_file.write_text(json.dumps(self._pools, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
92
|
+
|
|
93
|
+
def list_pools(self) -> list[dict]:
|
|
94
|
+
with self._lock:
|
|
95
|
+
return [dict(pool) for pool in self._pools]
|
|
96
|
+
|
|
97
|
+
def get_pool(self, pool_id: str) -> dict | None:
|
|
98
|
+
with self._lock:
|
|
99
|
+
for pool in self._pools:
|
|
100
|
+
if pool["id"] == pool_id:
|
|
101
|
+
return dict(pool)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def add_pool(self, name: str, base_url: str, secret_key: str) -> dict:
|
|
105
|
+
pool = _normalize_pool({"id": _new_id(), "name": name, "base_url": base_url, "secret_key": secret_key})
|
|
106
|
+
with self._lock:
|
|
107
|
+
self._pools.append(pool)
|
|
108
|
+
self._save()
|
|
109
|
+
return dict(pool)
|
|
110
|
+
|
|
111
|
+
def update_pool(self, pool_id: str, updates: dict) -> dict | None:
|
|
112
|
+
with self._lock:
|
|
113
|
+
for index, pool in enumerate(self._pools):
|
|
114
|
+
if pool["id"] != pool_id:
|
|
115
|
+
continue
|
|
116
|
+
merged = {**pool, **{key: value for key, value in updates.items() if value is not None}, "id": pool_id}
|
|
117
|
+
self._pools[index] = _normalize_pool(merged)
|
|
118
|
+
self._save()
|
|
119
|
+
return dict(self._pools[index])
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def delete_pool(self, pool_id: str) -> bool:
|
|
123
|
+
with self._lock:
|
|
124
|
+
before = len(self._pools)
|
|
125
|
+
self._pools = [pool for pool in self._pools if pool["id"] != pool_id]
|
|
126
|
+
if len(self._pools) < before:
|
|
127
|
+
self._save()
|
|
128
|
+
return True
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
def set_import_job(self, pool_id: str, import_job: dict | None) -> dict | None:
|
|
132
|
+
with self._lock:
|
|
133
|
+
for index, pool in enumerate(self._pools):
|
|
134
|
+
if pool["id"] != pool_id:
|
|
135
|
+
continue
|
|
136
|
+
next_pool = dict(pool)
|
|
137
|
+
next_pool["import_job"] = _normalize_import_job(import_job, fail_unfinished=False)
|
|
138
|
+
self._pools[index] = next_pool
|
|
139
|
+
self._save()
|
|
140
|
+
return dict(next_pool)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def get_import_job(self, pool_id: str) -> dict | None:
|
|
144
|
+
with self._lock:
|
|
145
|
+
for pool in self._pools:
|
|
146
|
+
if pool["id"] == pool_id:
|
|
147
|
+
job = pool.get("import_job")
|
|
148
|
+
return dict(job) if isinstance(job, dict) else None
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def list_remote_files(pool: dict) -> list[dict]:
|
|
153
|
+
base_url = str(pool.get("base_url") or "").strip()
|
|
154
|
+
secret_key = str(pool.get("secret_key") or "").strip()
|
|
155
|
+
if not base_url or not secret_key:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
url = f"{base_url.rstrip('/')}/v0/management/auth-files"
|
|
159
|
+
session = Session(**proxy_settings.build_session_kwargs(verify=True))
|
|
160
|
+
try:
|
|
161
|
+
response = session.get(url, headers=_management_headers(secret_key), timeout=30)
|
|
162
|
+
if not response.ok:
|
|
163
|
+
raise RuntimeError(f"remote list failed: HTTP {response.status_code}")
|
|
164
|
+
payload = response.json()
|
|
165
|
+
finally:
|
|
166
|
+
session.close()
|
|
167
|
+
|
|
168
|
+
files = payload.get("files") if isinstance(payload, dict) else None
|
|
169
|
+
if not isinstance(files, list):
|
|
170
|
+
raise RuntimeError("remote list payload is invalid")
|
|
171
|
+
|
|
172
|
+
items: list[dict] = []
|
|
173
|
+
for item in files:
|
|
174
|
+
if not isinstance(item, dict):
|
|
175
|
+
continue
|
|
176
|
+
name = str(item.get("name") or "").strip()
|
|
177
|
+
email = str(item.get("email") or item.get("account") or "").strip()
|
|
178
|
+
if not name:
|
|
179
|
+
continue
|
|
180
|
+
items.append({"name": name, "email": email})
|
|
181
|
+
return items
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def fetch_remote_access_token(pool: dict, file_name: str) -> tuple[str | None, str | None]:
|
|
185
|
+
base_url = str(pool.get("base_url") or "").strip()
|
|
186
|
+
secret_key = str(pool.get("secret_key") or "").strip()
|
|
187
|
+
file_name = str(file_name or "").strip()
|
|
188
|
+
if not base_url or not secret_key or not file_name:
|
|
189
|
+
return None, "invalid request"
|
|
190
|
+
|
|
191
|
+
url = f"{base_url.rstrip('/')}/v0/management/auth-files/download"
|
|
192
|
+
session = Session(**proxy_settings.build_session_kwargs(verify=True))
|
|
193
|
+
try:
|
|
194
|
+
response = session.get(url, headers=_management_headers(secret_key), params={"name": file_name}, timeout=30)
|
|
195
|
+
if not response.ok:
|
|
196
|
+
return None, f"HTTP {response.status_code}"
|
|
197
|
+
payload = response.json()
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
return None, str(exc)
|
|
200
|
+
finally:
|
|
201
|
+
session.close()
|
|
202
|
+
|
|
203
|
+
if not isinstance(payload, dict):
|
|
204
|
+
return None, "invalid payload"
|
|
205
|
+
|
|
206
|
+
access_token = str(payload.get("access_token") or "").strip()
|
|
207
|
+
if not access_token:
|
|
208
|
+
return None, "missing access_token"
|
|
209
|
+
return access_token, None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class CPAImportService:
|
|
213
|
+
def __init__(self, cpa_config: CPAConfig):
|
|
214
|
+
self._config = cpa_config
|
|
215
|
+
|
|
216
|
+
def start_import(self, pool: dict, selected_files: list[str]) -> dict:
|
|
217
|
+
names = [str(name or "").strip() for name in selected_files if str(name or "").strip()]
|
|
218
|
+
if not names:
|
|
219
|
+
raise ValueError("selected files is required")
|
|
220
|
+
|
|
221
|
+
pool_id = str(pool.get("id") or "").strip()
|
|
222
|
+
job = {
|
|
223
|
+
"job_id": uuid.uuid4().hex,
|
|
224
|
+
"status": "pending",
|
|
225
|
+
"created_at": _now_iso(),
|
|
226
|
+
"updated_at": _now_iso(),
|
|
227
|
+
"total": len(names),
|
|
228
|
+
"completed": 0,
|
|
229
|
+
"added": 0,
|
|
230
|
+
"skipped": 0,
|
|
231
|
+
"refreshed": 0,
|
|
232
|
+
"failed": 0,
|
|
233
|
+
"errors": [],
|
|
234
|
+
}
|
|
235
|
+
saved_pool = self._config.set_import_job(pool_id, job)
|
|
236
|
+
if saved_pool is None:
|
|
237
|
+
raise ValueError("pool not found")
|
|
238
|
+
|
|
239
|
+
thread = threading.Thread(
|
|
240
|
+
target=self._run_import,
|
|
241
|
+
args=(pool_id, pool, names),
|
|
242
|
+
name=f"cpa-import-{pool_id}",
|
|
243
|
+
daemon=True,
|
|
244
|
+
)
|
|
245
|
+
thread.start()
|
|
246
|
+
return dict(saved_pool.get("import_job") or job)
|
|
247
|
+
|
|
248
|
+
def _update_job(self, pool_id: str, **updates) -> dict | None:
|
|
249
|
+
current = self._config.get_import_job(pool_id)
|
|
250
|
+
if current is None:
|
|
251
|
+
return None
|
|
252
|
+
next_job = {**current, **updates, "updated_at": _now_iso()}
|
|
253
|
+
pool = self._config.set_import_job(pool_id, next_job)
|
|
254
|
+
if pool is None:
|
|
255
|
+
return None
|
|
256
|
+
job = pool.get("import_job")
|
|
257
|
+
return dict(job) if isinstance(job, dict) else None
|
|
258
|
+
|
|
259
|
+
def _append_error(self, pool_id: str, file_name: str, message: str) -> None:
|
|
260
|
+
current = self._config.get_import_job(pool_id)
|
|
261
|
+
if current is None:
|
|
262
|
+
return
|
|
263
|
+
errors = list(current.get("errors") or [])
|
|
264
|
+
errors.append({"name": file_name, "error": message})
|
|
265
|
+
self._update_job(pool_id, errors=errors, failed=len(errors))
|
|
266
|
+
|
|
267
|
+
def _run_import(self, pool_id: str, pool: dict, names: list[str]) -> None:
|
|
268
|
+
self._update_job(pool_id, status="running")
|
|
269
|
+
|
|
270
|
+
tokens: list[str] = []
|
|
271
|
+
max_workers = min(16, max(1, len(names)))
|
|
272
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
273
|
+
future_map = {executor.submit(fetch_remote_access_token, pool, name): name for name in names}
|
|
274
|
+
for future in as_completed(future_map):
|
|
275
|
+
file_name = future_map[future]
|
|
276
|
+
try:
|
|
277
|
+
token, error = future.result()
|
|
278
|
+
except Exception as exc:
|
|
279
|
+
token, error = None, str(exc)
|
|
280
|
+
|
|
281
|
+
if token:
|
|
282
|
+
tokens.append(token)
|
|
283
|
+
else:
|
|
284
|
+
self._append_error(pool_id, file_name, error or "unknown error")
|
|
285
|
+
|
|
286
|
+
current = self._config.get_import_job(pool_id) or {}
|
|
287
|
+
failed = len(current.get("errors") or [])
|
|
288
|
+
self._update_job(pool_id, completed=int(current.get("completed") or 0) + 1, failed=failed)
|
|
289
|
+
|
|
290
|
+
if not tokens:
|
|
291
|
+
current = self._config.get_import_job(pool_id) or {}
|
|
292
|
+
self._update_job(
|
|
293
|
+
pool_id,
|
|
294
|
+
status="failed",
|
|
295
|
+
completed=int(current.get("total") or 0),
|
|
296
|
+
failed=len(current.get("errors") or []),
|
|
297
|
+
)
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
add_result = account_service.add_accounts(tokens, source_type="codex")
|
|
301
|
+
refresh_result = account_service.refresh_accounts(tokens)
|
|
302
|
+
current = self._config.get_import_job(pool_id) or {}
|
|
303
|
+
self._update_job(
|
|
304
|
+
pool_id,
|
|
305
|
+
status="completed",
|
|
306
|
+
completed=len(names),
|
|
307
|
+
added=int(add_result.get("added") or 0),
|
|
308
|
+
skipped=int(add_result.get("skipped") or 0),
|
|
309
|
+
refreshed=int(refresh_result.get("refreshed") or 0),
|
|
310
|
+
failed=len(current.get("errors") or []),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
cpa_config = CPAConfig(CPA_CONFIG_FILE)
|
|
315
|
+
cpa_import_service = CPAImportService(cpa_config)
|