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,260 +1,260 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import threading
|
|
5
|
-
import time
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
from urllib.parse import quote
|
|
10
|
-
|
|
11
|
-
from services.account_service import account_service
|
|
12
|
-
from services.config import DATA_DIR
|
|
13
|
-
from services.content_filter import request_text
|
|
14
|
-
from services.log_service import LOG_TYPE_CALL, log_service
|
|
15
|
-
from services.openai_backend_api import EDITABLE_FILE_MODEL, OpenAIBackendAPI
|
|
16
|
-
from utils.helper import new_uuid
|
|
17
|
-
|
|
18
|
-
TASK_STATUS_QUEUED = "queued"
|
|
19
|
-
TASK_STATUS_RUNNING = "running"
|
|
20
|
-
TASK_STATUS_SUCCESS = "success"
|
|
21
|
-
TASK_STATUS_ERROR = "error"
|
|
22
|
-
UNFINISHED_STATUSES = {TASK_STATUS_QUEUED, TASK_STATUS_RUNNING}
|
|
23
|
-
EDITABLE_FILE_PLAN_TYPES = ("Plus", "Team", "Pro", "Enterprise")
|
|
24
|
-
EDITABLE_FILE_ROOT = DATA_DIR / "files"
|
|
25
|
-
EDITABLE_FILE_TASKS_PATH = DATA_DIR / "editable_file_tasks.json"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _now_iso() -> str:
|
|
29
|
-
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _clean(value: object, default: str = "") -> str:
|
|
33
|
-
return str(value or default).strip()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _owner_id(identity: dict[str, object]) -> str:
|
|
37
|
-
return _clean(identity.get("id")) or "anonymous"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _task_key(owner_id: str, task_id: str) -> str:
|
|
41
|
-
return f"{owner_id}:{task_id}"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _elapsed_seconds(task: dict[str, Any]) -> int:
|
|
45
|
-
start = float(task.get("started_ts") or task.get("created_ts") or 0)
|
|
46
|
-
end = float(task.get("ended_ts") or time.time())
|
|
47
|
-
return max(0, int(end - start)) if start else 0
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _file_url(path: Path, base_url: str) -> str:
|
|
51
|
-
rel = path.resolve().relative_to(EDITABLE_FILE_ROOT.resolve()).as_posix()
|
|
52
|
-
prefix = str(base_url or "").strip().rstrip("/")
|
|
53
|
-
return f"{prefix}/files/{quote(rel, safe='/')}" if prefix else f"/files/{quote(rel, safe='/')}"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _editable_access_token() -> str:
|
|
57
|
-
accounts = [
|
|
58
|
-
item for item in account_service.list_accounts()
|
|
59
|
-
if _clean(item.get("access_token"))
|
|
60
|
-
and item.get("status") not in {"禁用", "异常"}
|
|
61
|
-
and account_service._account_matches_any_plan_type(item, EDITABLE_FILE_PLAN_TYPES)
|
|
62
|
-
]
|
|
63
|
-
if not accounts:
|
|
64
|
-
raise RuntimeError("no available plus/team/pro account")
|
|
65
|
-
accounts.sort(key=lambda item: _clean(item.get("last_used_at")))
|
|
66
|
-
token = _clean(accounts[0].get("access_token"))
|
|
67
|
-
return account_service.refresh_access_token(token, event="editable_file_task") or token
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _public_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
71
|
-
item = {
|
|
72
|
-
"id": task.get("id"),
|
|
73
|
-
"taskId": task.get("id"),
|
|
74
|
-
"status": task.get("status"),
|
|
75
|
-
"kind": task.get("kind"),
|
|
76
|
-
"created_at": task.get("created_at"),
|
|
77
|
-
"updated_at": task.get("updated_at"),
|
|
78
|
-
"elapsed_seconds": _elapsed_seconds(task),
|
|
79
|
-
}
|
|
80
|
-
for key in ("result", "error"):
|
|
81
|
-
if task.get(key):
|
|
82
|
-
item[key] = task[key]
|
|
83
|
-
return item
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class EditableFileTaskService:
|
|
87
|
-
def __init__(self, path: Path = EDITABLE_FILE_TASKS_PATH) -> None:
|
|
88
|
-
self.path = path
|
|
89
|
-
self._lock = threading.RLock()
|
|
90
|
-
self._tasks: dict[str, dict[str, Any]] = {}
|
|
91
|
-
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
-
with self._lock:
|
|
93
|
-
self._tasks = self._load_locked()
|
|
94
|
-
if self._recover_unfinished_locked():
|
|
95
|
-
self._save_locked()
|
|
96
|
-
|
|
97
|
-
def submit_ppt(self, identity: dict[str, object], *, client_task_id: str = "", prompt: str = "", base64_images: list[str] | None = None, base_url: str = "") -> dict[str, Any]:
|
|
98
|
-
return self._submit(identity, client_task_id=client_task_id, kind="ppt", prompt=prompt, base64_images=base64_images or [], base_url=base_url)
|
|
99
|
-
|
|
100
|
-
def submit_psd(self, identity: dict[str, object], *, client_task_id: str = "", prompt: str = "", base64_images: list[str] | None = None, base_url: str = "") -> dict[str, Any]:
|
|
101
|
-
return self._submit(identity, client_task_id=client_task_id, kind="psd", prompt=prompt, base64_images=base64_images or [], base_url=base_url)
|
|
102
|
-
|
|
103
|
-
def list_tasks(self, identity: dict[str, object], task_ids: list[str]) -> dict[str, Any]:
|
|
104
|
-
owner = _owner_id(identity)
|
|
105
|
-
requested = [_clean(item) for item in task_ids if _clean(item)]
|
|
106
|
-
with self._lock:
|
|
107
|
-
if requested:
|
|
108
|
-
items = [task for task_id in requested if (task := self._tasks.get(_task_key(owner, task_id)))]
|
|
109
|
-
return {"items": [_public_task(item) for item in items], "missing_ids": [task_id for task_id in requested if _task_key(owner, task_id) not in self._tasks]}
|
|
110
|
-
items = [task for task in self._tasks.values() if task.get("owner_id") == owner]
|
|
111
|
-
items.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
|
|
112
|
-
return {"items": [_public_task(item) for item in items], "missing_ids": []}
|
|
113
|
-
|
|
114
|
-
def _submit(self, identity: dict[str, object], *, client_task_id: str, kind: str, prompt: str, base64_images: list[str], base_url: str) -> dict[str, Any]:
|
|
115
|
-
task_id = _clean(client_task_id) or new_uuid()
|
|
116
|
-
owner = _owner_id(identity)
|
|
117
|
-
key = _task_key(owner, task_id)
|
|
118
|
-
now = _now_iso()
|
|
119
|
-
with self._lock:
|
|
120
|
-
if key in self._tasks:
|
|
121
|
-
return _public_task(self._tasks[key])
|
|
122
|
-
ts = time.time()
|
|
123
|
-
self._tasks[key] = {"id": task_id, "owner_id": owner, "status": TASK_STATUS_QUEUED, "kind": kind, "model": EDITABLE_FILE_MODEL, "created_at": now, "updated_at": now, "created_ts": ts, "updated_ts": ts}
|
|
124
|
-
task = dict(self._tasks[key])
|
|
125
|
-
self._save_locked()
|
|
126
|
-
threading.Thread(target=self._run_task, args=(key, kind, prompt, base64_images, dict(identity), base_url), name=f"{kind}-file-task-{task_id[:16]}", daemon=True).start()
|
|
127
|
-
return _public_task(task)
|
|
128
|
-
|
|
129
|
-
def _run_task(self, key: str, kind: str, prompt: str, base64_images: list[str], identity: dict[str, object], base_url: str) -> None:
|
|
130
|
-
started = time.time()
|
|
131
|
-
token = ""
|
|
132
|
-
account_email = ""
|
|
133
|
-
self._update_task(key, status=TASK_STATUS_RUNNING, error="", started_ts=started)
|
|
134
|
-
backend = None
|
|
135
|
-
try:
|
|
136
|
-
if kind == "psd" and not base64_images:
|
|
137
|
-
raise ValueError("base64_images is empty")
|
|
138
|
-
token = _editable_access_token()
|
|
139
|
-
account = account_service.get_account(token) or {}
|
|
140
|
-
account_email = _clean(account.get("email"))
|
|
141
|
-
backend = OpenAIBackendAPI(token)
|
|
142
|
-
output_dir = EDITABLE_FILE_ROOT / kind / key.rsplit(":", 1)[-1]
|
|
143
|
-
result = backend.export_psd_zip(base64_images, prompt, output_dir) if kind == "psd" else backend.export_ppt_zip(base64_images, prompt, output_dir)
|
|
144
|
-
account_service.mark_text_used(token)
|
|
145
|
-
data = {"conversation_id": result.conversation_id, "primary_url": _file_url(result.primary_path, base_url), "zip_url": _file_url(result.zip_path, base_url)}
|
|
146
|
-
self._update_task(key, status=TASK_STATUS_SUCCESS, result=data, account_email=account_email, error="", ended_ts=time.time())
|
|
147
|
-
self._log_call(identity, kind, started, request_text(prompt), account_email=account_email, result=data)
|
|
148
|
-
except Exception as exc:
|
|
149
|
-
error = str(exc) or "editable file task failed"
|
|
150
|
-
self._update_task(key, status=TASK_STATUS_ERROR, error=error, account_email=account_email, ended_ts=time.time())
|
|
151
|
-
self._log_call(identity, kind, started, request_text(prompt), status="failed", error=error, account_email=account_email)
|
|
152
|
-
finally:
|
|
153
|
-
if backend is not None:
|
|
154
|
-
backend.close()
|
|
155
|
-
|
|
156
|
-
def public_file_path(self, relative_path: str) -> Path:
|
|
157
|
-
raw = str(relative_path or "").replace("\\", "/").lstrip("/")
|
|
158
|
-
path = (EDITABLE_FILE_ROOT / raw).resolve()
|
|
159
|
-
path.relative_to(EDITABLE_FILE_ROOT.resolve())
|
|
160
|
-
if not path.is_file():
|
|
161
|
-
raise FileNotFoundError(raw)
|
|
162
|
-
return path
|
|
163
|
-
|
|
164
|
-
def _update_task(self, key: str, **updates: Any) -> None:
|
|
165
|
-
with self._lock:
|
|
166
|
-
task = self._tasks.get(key)
|
|
167
|
-
if task is None:
|
|
168
|
-
return
|
|
169
|
-
task.update(updates)
|
|
170
|
-
task["updated_at"] = _now_iso()
|
|
171
|
-
task["updated_ts"] = time.time()
|
|
172
|
-
self._save_locked()
|
|
173
|
-
|
|
174
|
-
def _load_locked(self) -> dict[str, dict[str, Any]]:
|
|
175
|
-
if not self.path.exists():
|
|
176
|
-
return {}
|
|
177
|
-
try:
|
|
178
|
-
raw = json.loads(self.path.read_text(encoding="utf-8"))
|
|
179
|
-
except Exception:
|
|
180
|
-
return {}
|
|
181
|
-
tasks: dict[str, dict[str, Any]] = {}
|
|
182
|
-
for item in (raw.get("tasks") if isinstance(raw, dict) else raw) or []:
|
|
183
|
-
if not isinstance(item, dict):
|
|
184
|
-
continue
|
|
185
|
-
task_id = _clean(item.get("id"))
|
|
186
|
-
owner = _clean(item.get("owner_id"))
|
|
187
|
-
if not task_id or not owner:
|
|
188
|
-
continue
|
|
189
|
-
task = {
|
|
190
|
-
"id": task_id,
|
|
191
|
-
"owner_id": owner,
|
|
192
|
-
"status": _clean(item.get("status"), TASK_STATUS_ERROR),
|
|
193
|
-
"kind": "psd" if item.get("kind") == "psd" else "ppt",
|
|
194
|
-
"created_at": _clean(item.get("created_at"), _now_iso()),
|
|
195
|
-
"updated_at": _clean(item.get("updated_at"), _clean(item.get("created_at"), _now_iso())),
|
|
196
|
-
"created_ts": float(item.get("created_ts") or 0),
|
|
197
|
-
"updated_ts": float(item.get("updated_ts") or 0),
|
|
198
|
-
}
|
|
199
|
-
for field in ("result", "error", "started_ts", "ended_ts"):
|
|
200
|
-
if item.get(field):
|
|
201
|
-
task[field] = item[field]
|
|
202
|
-
tasks[_task_key(owner, task_id)] = task
|
|
203
|
-
return tasks
|
|
204
|
-
|
|
205
|
-
def _save_locked(self) -> None:
|
|
206
|
-
items = sorted(self._tasks.values(), key=lambda item: str(item.get("updated_at") or ""), reverse=True)
|
|
207
|
-
tmp_path = self.path.with_suffix(self.path.suffix + ".tmp")
|
|
208
|
-
tmp_path.write_text(json.dumps({"tasks": items}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
209
|
-
tmp_path.replace(self.path)
|
|
210
|
-
|
|
211
|
-
def _recover_unfinished_locked(self) -> bool:
|
|
212
|
-
changed = False
|
|
213
|
-
for task in self._tasks.values():
|
|
214
|
-
if task.get("status") in UNFINISHED_STATUSES:
|
|
215
|
-
task["status"] = TASK_STATUS_ERROR
|
|
216
|
-
task["error"] = "服务已重启,未完成的任务已中断"
|
|
217
|
-
task["ended_ts"] = time.time()
|
|
218
|
-
task["updated_at"] = _now_iso()
|
|
219
|
-
task["updated_ts"] = time.time()
|
|
220
|
-
changed = True
|
|
221
|
-
return changed
|
|
222
|
-
|
|
223
|
-
def _log_call(
|
|
224
|
-
self,
|
|
225
|
-
identity: dict[str, object],
|
|
226
|
-
kind: str,
|
|
227
|
-
started: float,
|
|
228
|
-
request_preview: str,
|
|
229
|
-
*,
|
|
230
|
-
status: str = "success",
|
|
231
|
-
error: str = "",
|
|
232
|
-
account_email: str = "",
|
|
233
|
-
result: dict[str, str] | None = None,
|
|
234
|
-
) -> None:
|
|
235
|
-
detail = {
|
|
236
|
-
"key_id": identity.get("id"),
|
|
237
|
-
"key_name": identity.get("name"),
|
|
238
|
-
"role": identity.get("role"),
|
|
239
|
-
"endpoint": f"/v1/{kind}/generations",
|
|
240
|
-
"model": EDITABLE_FILE_MODEL,
|
|
241
|
-
"started_at": datetime.fromtimestamp(started).strftime("%Y-%m-%d %H:%M:%S"),
|
|
242
|
-
"ended_at": _now_iso(),
|
|
243
|
-
"duration_ms": int((time.time() - started) * 1000),
|
|
244
|
-
"status": status,
|
|
245
|
-
}
|
|
246
|
-
if request_preview:
|
|
247
|
-
detail["request_text"] = request_preview
|
|
248
|
-
if account_email:
|
|
249
|
-
detail["account_email"] = account_email
|
|
250
|
-
if error:
|
|
251
|
-
detail["error"] = error
|
|
252
|
-
if result:
|
|
253
|
-
detail["result"] = result
|
|
254
|
-
try:
|
|
255
|
-
log_service.add(LOG_TYPE_CALL, f"{kind.upper()}生成任务{'失败' if status == 'failed' else '完成'}", detail)
|
|
256
|
-
except Exception:
|
|
257
|
-
pass
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
editable_file_task_service = EditableFileTaskService()
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
from services.account_service import account_service
|
|
12
|
+
from services.config import DATA_DIR
|
|
13
|
+
from services.content_filter import request_text
|
|
14
|
+
from services.log_service import LOG_TYPE_CALL, log_service
|
|
15
|
+
from services.openai_backend_api import EDITABLE_FILE_MODEL, OpenAIBackendAPI
|
|
16
|
+
from utils.helper import new_uuid
|
|
17
|
+
|
|
18
|
+
TASK_STATUS_QUEUED = "queued"
|
|
19
|
+
TASK_STATUS_RUNNING = "running"
|
|
20
|
+
TASK_STATUS_SUCCESS = "success"
|
|
21
|
+
TASK_STATUS_ERROR = "error"
|
|
22
|
+
UNFINISHED_STATUSES = {TASK_STATUS_QUEUED, TASK_STATUS_RUNNING}
|
|
23
|
+
EDITABLE_FILE_PLAN_TYPES = ("Plus", "Team", "Pro", "Enterprise")
|
|
24
|
+
EDITABLE_FILE_ROOT = DATA_DIR / "files"
|
|
25
|
+
EDITABLE_FILE_TASKS_PATH = DATA_DIR / "editable_file_tasks.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _now_iso() -> str:
|
|
29
|
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _clean(value: object, default: str = "") -> str:
|
|
33
|
+
return str(value or default).strip()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _owner_id(identity: dict[str, object]) -> str:
|
|
37
|
+
return _clean(identity.get("id")) or "anonymous"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _task_key(owner_id: str, task_id: str) -> str:
|
|
41
|
+
return f"{owner_id}:{task_id}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _elapsed_seconds(task: dict[str, Any]) -> int:
|
|
45
|
+
start = float(task.get("started_ts") or task.get("created_ts") or 0)
|
|
46
|
+
end = float(task.get("ended_ts") or time.time())
|
|
47
|
+
return max(0, int(end - start)) if start else 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _file_url(path: Path, base_url: str) -> str:
|
|
51
|
+
rel = path.resolve().relative_to(EDITABLE_FILE_ROOT.resolve()).as_posix()
|
|
52
|
+
prefix = str(base_url or "").strip().rstrip("/")
|
|
53
|
+
return f"{prefix}/files/{quote(rel, safe='/')}" if prefix else f"/files/{quote(rel, safe='/')}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _editable_access_token() -> str:
|
|
57
|
+
accounts = [
|
|
58
|
+
item for item in account_service.list_accounts()
|
|
59
|
+
if _clean(item.get("access_token"))
|
|
60
|
+
and item.get("status") not in {"禁用", "异常"}
|
|
61
|
+
and account_service._account_matches_any_plan_type(item, EDITABLE_FILE_PLAN_TYPES)
|
|
62
|
+
]
|
|
63
|
+
if not accounts:
|
|
64
|
+
raise RuntimeError("no available plus/team/pro account")
|
|
65
|
+
accounts.sort(key=lambda item: _clean(item.get("last_used_at")))
|
|
66
|
+
token = _clean(accounts[0].get("access_token"))
|
|
67
|
+
return account_service.refresh_access_token(token, event="editable_file_task") or token
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _public_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
71
|
+
item = {
|
|
72
|
+
"id": task.get("id"),
|
|
73
|
+
"taskId": task.get("id"),
|
|
74
|
+
"status": task.get("status"),
|
|
75
|
+
"kind": task.get("kind"),
|
|
76
|
+
"created_at": task.get("created_at"),
|
|
77
|
+
"updated_at": task.get("updated_at"),
|
|
78
|
+
"elapsed_seconds": _elapsed_seconds(task),
|
|
79
|
+
}
|
|
80
|
+
for key in ("result", "error"):
|
|
81
|
+
if task.get(key):
|
|
82
|
+
item[key] = task[key]
|
|
83
|
+
return item
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class EditableFileTaskService:
|
|
87
|
+
def __init__(self, path: Path = EDITABLE_FILE_TASKS_PATH) -> None:
|
|
88
|
+
self.path = path
|
|
89
|
+
self._lock = threading.RLock()
|
|
90
|
+
self._tasks: dict[str, dict[str, Any]] = {}
|
|
91
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
with self._lock:
|
|
93
|
+
self._tasks = self._load_locked()
|
|
94
|
+
if self._recover_unfinished_locked():
|
|
95
|
+
self._save_locked()
|
|
96
|
+
|
|
97
|
+
def submit_ppt(self, identity: dict[str, object], *, client_task_id: str = "", prompt: str = "", base64_images: list[str] | None = None, base_url: str = "") -> dict[str, Any]:
|
|
98
|
+
return self._submit(identity, client_task_id=client_task_id, kind="ppt", prompt=prompt, base64_images=base64_images or [], base_url=base_url)
|
|
99
|
+
|
|
100
|
+
def submit_psd(self, identity: dict[str, object], *, client_task_id: str = "", prompt: str = "", base64_images: list[str] | None = None, base_url: str = "") -> dict[str, Any]:
|
|
101
|
+
return self._submit(identity, client_task_id=client_task_id, kind="psd", prompt=prompt, base64_images=base64_images or [], base_url=base_url)
|
|
102
|
+
|
|
103
|
+
def list_tasks(self, identity: dict[str, object], task_ids: list[str]) -> dict[str, Any]:
|
|
104
|
+
owner = _owner_id(identity)
|
|
105
|
+
requested = [_clean(item) for item in task_ids if _clean(item)]
|
|
106
|
+
with self._lock:
|
|
107
|
+
if requested:
|
|
108
|
+
items = [task for task_id in requested if (task := self._tasks.get(_task_key(owner, task_id)))]
|
|
109
|
+
return {"items": [_public_task(item) for item in items], "missing_ids": [task_id for task_id in requested if _task_key(owner, task_id) not in self._tasks]}
|
|
110
|
+
items = [task for task in self._tasks.values() if task.get("owner_id") == owner]
|
|
111
|
+
items.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
|
|
112
|
+
return {"items": [_public_task(item) for item in items], "missing_ids": []}
|
|
113
|
+
|
|
114
|
+
def _submit(self, identity: dict[str, object], *, client_task_id: str, kind: str, prompt: str, base64_images: list[str], base_url: str) -> dict[str, Any]:
|
|
115
|
+
task_id = _clean(client_task_id) or new_uuid()
|
|
116
|
+
owner = _owner_id(identity)
|
|
117
|
+
key = _task_key(owner, task_id)
|
|
118
|
+
now = _now_iso()
|
|
119
|
+
with self._lock:
|
|
120
|
+
if key in self._tasks:
|
|
121
|
+
return _public_task(self._tasks[key])
|
|
122
|
+
ts = time.time()
|
|
123
|
+
self._tasks[key] = {"id": task_id, "owner_id": owner, "status": TASK_STATUS_QUEUED, "kind": kind, "model": EDITABLE_FILE_MODEL, "created_at": now, "updated_at": now, "created_ts": ts, "updated_ts": ts}
|
|
124
|
+
task = dict(self._tasks[key])
|
|
125
|
+
self._save_locked()
|
|
126
|
+
threading.Thread(target=self._run_task, args=(key, kind, prompt, base64_images, dict(identity), base_url), name=f"{kind}-file-task-{task_id[:16]}", daemon=True).start()
|
|
127
|
+
return _public_task(task)
|
|
128
|
+
|
|
129
|
+
def _run_task(self, key: str, kind: str, prompt: str, base64_images: list[str], identity: dict[str, object], base_url: str) -> None:
|
|
130
|
+
started = time.time()
|
|
131
|
+
token = ""
|
|
132
|
+
account_email = ""
|
|
133
|
+
self._update_task(key, status=TASK_STATUS_RUNNING, error="", started_ts=started)
|
|
134
|
+
backend = None
|
|
135
|
+
try:
|
|
136
|
+
if kind == "psd" and not base64_images:
|
|
137
|
+
raise ValueError("base64_images is empty")
|
|
138
|
+
token = _editable_access_token()
|
|
139
|
+
account = account_service.get_account(token) or {}
|
|
140
|
+
account_email = _clean(account.get("email"))
|
|
141
|
+
backend = OpenAIBackendAPI(token)
|
|
142
|
+
output_dir = EDITABLE_FILE_ROOT / kind / key.rsplit(":", 1)[-1]
|
|
143
|
+
result = backend.export_psd_zip(base64_images, prompt, output_dir) if kind == "psd" else backend.export_ppt_zip(base64_images, prompt, output_dir)
|
|
144
|
+
account_service.mark_text_used(token)
|
|
145
|
+
data = {"conversation_id": result.conversation_id, "primary_url": _file_url(result.primary_path, base_url), "zip_url": _file_url(result.zip_path, base_url)}
|
|
146
|
+
self._update_task(key, status=TASK_STATUS_SUCCESS, result=data, account_email=account_email, error="", ended_ts=time.time())
|
|
147
|
+
self._log_call(identity, kind, started, request_text(prompt), account_email=account_email, result=data)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
error = str(exc) or "editable file task failed"
|
|
150
|
+
self._update_task(key, status=TASK_STATUS_ERROR, error=error, account_email=account_email, ended_ts=time.time())
|
|
151
|
+
self._log_call(identity, kind, started, request_text(prompt), status="failed", error=error, account_email=account_email)
|
|
152
|
+
finally:
|
|
153
|
+
if backend is not None:
|
|
154
|
+
backend.close()
|
|
155
|
+
|
|
156
|
+
def public_file_path(self, relative_path: str) -> Path:
|
|
157
|
+
raw = str(relative_path or "").replace("\\", "/").lstrip("/")
|
|
158
|
+
path = (EDITABLE_FILE_ROOT / raw).resolve()
|
|
159
|
+
path.relative_to(EDITABLE_FILE_ROOT.resolve())
|
|
160
|
+
if not path.is_file():
|
|
161
|
+
raise FileNotFoundError(raw)
|
|
162
|
+
return path
|
|
163
|
+
|
|
164
|
+
def _update_task(self, key: str, **updates: Any) -> None:
|
|
165
|
+
with self._lock:
|
|
166
|
+
task = self._tasks.get(key)
|
|
167
|
+
if task is None:
|
|
168
|
+
return
|
|
169
|
+
task.update(updates)
|
|
170
|
+
task["updated_at"] = _now_iso()
|
|
171
|
+
task["updated_ts"] = time.time()
|
|
172
|
+
self._save_locked()
|
|
173
|
+
|
|
174
|
+
def _load_locked(self) -> dict[str, dict[str, Any]]:
|
|
175
|
+
if not self.path.exists():
|
|
176
|
+
return {}
|
|
177
|
+
try:
|
|
178
|
+
raw = json.loads(self.path.read_text(encoding="utf-8"))
|
|
179
|
+
except Exception:
|
|
180
|
+
return {}
|
|
181
|
+
tasks: dict[str, dict[str, Any]] = {}
|
|
182
|
+
for item in (raw.get("tasks") if isinstance(raw, dict) else raw) or []:
|
|
183
|
+
if not isinstance(item, dict):
|
|
184
|
+
continue
|
|
185
|
+
task_id = _clean(item.get("id"))
|
|
186
|
+
owner = _clean(item.get("owner_id"))
|
|
187
|
+
if not task_id or not owner:
|
|
188
|
+
continue
|
|
189
|
+
task = {
|
|
190
|
+
"id": task_id,
|
|
191
|
+
"owner_id": owner,
|
|
192
|
+
"status": _clean(item.get("status"), TASK_STATUS_ERROR),
|
|
193
|
+
"kind": "psd" if item.get("kind") == "psd" else "ppt",
|
|
194
|
+
"created_at": _clean(item.get("created_at"), _now_iso()),
|
|
195
|
+
"updated_at": _clean(item.get("updated_at"), _clean(item.get("created_at"), _now_iso())),
|
|
196
|
+
"created_ts": float(item.get("created_ts") or 0),
|
|
197
|
+
"updated_ts": float(item.get("updated_ts") or 0),
|
|
198
|
+
}
|
|
199
|
+
for field in ("result", "error", "started_ts", "ended_ts"):
|
|
200
|
+
if item.get(field):
|
|
201
|
+
task[field] = item[field]
|
|
202
|
+
tasks[_task_key(owner, task_id)] = task
|
|
203
|
+
return tasks
|
|
204
|
+
|
|
205
|
+
def _save_locked(self) -> None:
|
|
206
|
+
items = sorted(self._tasks.values(), key=lambda item: str(item.get("updated_at") or ""), reverse=True)
|
|
207
|
+
tmp_path = self.path.with_suffix(self.path.suffix + ".tmp")
|
|
208
|
+
tmp_path.write_text(json.dumps({"tasks": items}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
209
|
+
tmp_path.replace(self.path)
|
|
210
|
+
|
|
211
|
+
def _recover_unfinished_locked(self) -> bool:
|
|
212
|
+
changed = False
|
|
213
|
+
for task in self._tasks.values():
|
|
214
|
+
if task.get("status") in UNFINISHED_STATUSES:
|
|
215
|
+
task["status"] = TASK_STATUS_ERROR
|
|
216
|
+
task["error"] = "服务已重启,未完成的任务已中断"
|
|
217
|
+
task["ended_ts"] = time.time()
|
|
218
|
+
task["updated_at"] = _now_iso()
|
|
219
|
+
task["updated_ts"] = time.time()
|
|
220
|
+
changed = True
|
|
221
|
+
return changed
|
|
222
|
+
|
|
223
|
+
def _log_call(
|
|
224
|
+
self,
|
|
225
|
+
identity: dict[str, object],
|
|
226
|
+
kind: str,
|
|
227
|
+
started: float,
|
|
228
|
+
request_preview: str,
|
|
229
|
+
*,
|
|
230
|
+
status: str = "success",
|
|
231
|
+
error: str = "",
|
|
232
|
+
account_email: str = "",
|
|
233
|
+
result: dict[str, str] | None = None,
|
|
234
|
+
) -> None:
|
|
235
|
+
detail = {
|
|
236
|
+
"key_id": identity.get("id"),
|
|
237
|
+
"key_name": identity.get("name"),
|
|
238
|
+
"role": identity.get("role"),
|
|
239
|
+
"endpoint": f"/v1/{kind}/generations",
|
|
240
|
+
"model": EDITABLE_FILE_MODEL,
|
|
241
|
+
"started_at": datetime.fromtimestamp(started).strftime("%Y-%m-%d %H:%M:%S"),
|
|
242
|
+
"ended_at": _now_iso(),
|
|
243
|
+
"duration_ms": int((time.time() - started) * 1000),
|
|
244
|
+
"status": status,
|
|
245
|
+
}
|
|
246
|
+
if request_preview:
|
|
247
|
+
detail["request_text"] = request_preview
|
|
248
|
+
if account_email:
|
|
249
|
+
detail["account_email"] = account_email
|
|
250
|
+
if error:
|
|
251
|
+
detail["error"] = error
|
|
252
|
+
if result:
|
|
253
|
+
detail["result"] = result
|
|
254
|
+
try:
|
|
255
|
+
log_service.add(LOG_TYPE_CALL, f"{kind.upper()}生成任务{'失败' if status == 'failed' else '完成'}", detail)
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
editable_file_task_service = EditableFileTaskService()
|