dsh-plugin-image-workbench 1.1.5 → 1.1.6
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 +1329 -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/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,337 +1,337 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
import json
|
|
5
|
-
import itertools
|
|
6
|
-
import time
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
|
-
from uuid import uuid4
|
|
12
|
-
|
|
13
|
-
from fastapi import HTTPException
|
|
14
|
-
from fastapi.concurrency import run_in_threadpool
|
|
15
|
-
from fastapi.responses import JSONResponse, StreamingResponse
|
|
16
|
-
|
|
17
|
-
from services.config import DATA_DIR
|
|
18
|
-
from services.protocol.error_response import anthropic_error_response, openai_error_response
|
|
19
|
-
from utils.helper import anthropic_sse_stream, sse_json_stream
|
|
20
|
-
|
|
21
|
-
LOG_TYPE_CALL = "call"
|
|
22
|
-
LOG_TYPE_ACCOUNT = "account"
|
|
23
|
-
INTERNAL_RESPONSE_KEYS = {"_account_email", "_conversation_id"}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class LogService:
|
|
27
|
-
def __init__(self, path: Path):
|
|
28
|
-
self.path = path
|
|
29
|
-
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
-
|
|
31
|
-
@staticmethod
|
|
32
|
-
def _legacy_id(raw_line: str, line_number: int) -> str:
|
|
33
|
-
payload = f"{line_number}:{raw_line}".encode("utf-8", errors="ignore")
|
|
34
|
-
return hashlib.sha1(payload).hexdigest()[:24]
|
|
35
|
-
|
|
36
|
-
def _parse_line(self, raw_line: str, line_number: int) -> dict[str, Any] | None:
|
|
37
|
-
try:
|
|
38
|
-
item = json.loads(raw_line)
|
|
39
|
-
except Exception:
|
|
40
|
-
return None
|
|
41
|
-
if not isinstance(item, dict):
|
|
42
|
-
return None
|
|
43
|
-
parsed = dict(item)
|
|
44
|
-
parsed["id"] = str(parsed.get("id") or self._legacy_id(raw_line, line_number))
|
|
45
|
-
return parsed
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def _serialize_item(item: dict[str, Any]) -> str:
|
|
49
|
-
return json.dumps(item, ensure_ascii=False, separators=(",", ":"))
|
|
50
|
-
|
|
51
|
-
@staticmethod
|
|
52
|
-
def _matches_filters(item: dict[str, Any], *, type: str = "", start_date: str = "", end_date: str = "") -> bool:
|
|
53
|
-
t = str(item.get("time") or "")
|
|
54
|
-
day = t[:10]
|
|
55
|
-
if type and item.get("type") != type:
|
|
56
|
-
return False
|
|
57
|
-
if start_date and day < start_date:
|
|
58
|
-
return False
|
|
59
|
-
if end_date and day > end_date:
|
|
60
|
-
return False
|
|
61
|
-
return True
|
|
62
|
-
|
|
63
|
-
def add(self, type: str, summary: str = "", detail: dict[str, Any] | None = None, **data: Any) -> None:
|
|
64
|
-
item = {
|
|
65
|
-
"id": uuid4().hex,
|
|
66
|
-
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
67
|
-
"type": type,
|
|
68
|
-
"summary": summary,
|
|
69
|
-
"detail": detail or data,
|
|
70
|
-
}
|
|
71
|
-
with self.path.open("a", encoding="utf-8") as file:
|
|
72
|
-
file.write(self._serialize_item(item) + "\n")
|
|
73
|
-
|
|
74
|
-
def list(self, type: str = "", start_date: str = "", end_date: str = "", limit: int = 200) -> list[dict[str, Any]]:
|
|
75
|
-
if not self.path.exists():
|
|
76
|
-
return []
|
|
77
|
-
items: list[dict[str, Any]] = []
|
|
78
|
-
lines = self.path.read_text(encoding="utf-8").splitlines()
|
|
79
|
-
for line_number in range(len(lines) - 1, -1, -1):
|
|
80
|
-
item = self._parse_line(lines[line_number], line_number)
|
|
81
|
-
if item is None:
|
|
82
|
-
continue
|
|
83
|
-
if not self._matches_filters(item, type=type, start_date=start_date, end_date=end_date):
|
|
84
|
-
continue
|
|
85
|
-
items.append(item)
|
|
86
|
-
if len(items) >= limit:
|
|
87
|
-
break
|
|
88
|
-
return items
|
|
89
|
-
|
|
90
|
-
def delete(self, ids: list[str]) -> dict[str, int]:
|
|
91
|
-
target_ids = {str(item or "").strip() for item in ids if str(item or "").strip()}
|
|
92
|
-
if not self.path.exists() or not target_ids:
|
|
93
|
-
return {"removed": 0}
|
|
94
|
-
lines = self.path.read_text(encoding="utf-8").splitlines()
|
|
95
|
-
kept_lines: list[str] = []
|
|
96
|
-
removed = 0
|
|
97
|
-
for line_number, raw_line in enumerate(lines):
|
|
98
|
-
item = self._parse_line(raw_line, line_number)
|
|
99
|
-
if item is None:
|
|
100
|
-
kept_lines.append(raw_line)
|
|
101
|
-
continue
|
|
102
|
-
if str(item.get("id") or "") in target_ids:
|
|
103
|
-
removed += 1
|
|
104
|
-
continue
|
|
105
|
-
kept_lines.append(self._serialize_item(item))
|
|
106
|
-
content = "\n".join(kept_lines)
|
|
107
|
-
if content:
|
|
108
|
-
content += "\n"
|
|
109
|
-
self.path.write_text(content, encoding="utf-8")
|
|
110
|
-
return {"removed": removed}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
log_service = LogService(DATA_DIR / "logs.jsonl")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _collect_urls(value: object) -> list[str]:
|
|
117
|
-
urls: list[str] = []
|
|
118
|
-
if isinstance(value, dict):
|
|
119
|
-
for key, item in value.items():
|
|
120
|
-
if key == "url" and isinstance(item, str):
|
|
121
|
-
urls.append(item)
|
|
122
|
-
elif key == "urls" and isinstance(item, list):
|
|
123
|
-
urls.extend(str(url) for url in item if isinstance(url, str))
|
|
124
|
-
else:
|
|
125
|
-
urls.extend(_collect_urls(item))
|
|
126
|
-
elif isinstance(value, list):
|
|
127
|
-
for item in value:
|
|
128
|
-
urls.extend(_collect_urls(item))
|
|
129
|
-
return urls
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _collect_account_emails(value: object) -> list[str]:
|
|
133
|
-
emails: list[str] = []
|
|
134
|
-
if isinstance(value, dict):
|
|
135
|
-
for key, item in value.items():
|
|
136
|
-
if key in {"_account_email", "account_email"} and isinstance(item, str) and item.strip():
|
|
137
|
-
emails.append(item.strip())
|
|
138
|
-
else:
|
|
139
|
-
emails.extend(_collect_account_emails(item))
|
|
140
|
-
elif isinstance(value, list):
|
|
141
|
-
for item in value:
|
|
142
|
-
emails.extend(_collect_account_emails(item))
|
|
143
|
-
return emails
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _collect_conversation_ids(value: object) -> list[str]:
|
|
147
|
-
ids: list[str] = []
|
|
148
|
-
if isinstance(value, dict):
|
|
149
|
-
for key, item in value.items():
|
|
150
|
-
if key == "_conversation_id" and isinstance(item, str) and item.strip():
|
|
151
|
-
ids.append(item.strip())
|
|
152
|
-
else:
|
|
153
|
-
ids.extend(_collect_conversation_ids(item))
|
|
154
|
-
elif isinstance(value, list):
|
|
155
|
-
for item in value:
|
|
156
|
-
ids.extend(_collect_conversation_ids(item))
|
|
157
|
-
return ids
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _strip_internal_response_fields(value: object) -> object:
|
|
161
|
-
if isinstance(value, dict):
|
|
162
|
-
return {
|
|
163
|
-
key: _strip_internal_response_fields(item)
|
|
164
|
-
for key, item in value.items()
|
|
165
|
-
if key not in INTERNAL_RESPONSE_KEYS
|
|
166
|
-
}
|
|
167
|
-
if isinstance(value, list):
|
|
168
|
-
return [_strip_internal_response_fields(item) for item in value]
|
|
169
|
-
return value
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _request_excerpt(text: object, limit: int = 1000) -> str:
|
|
173
|
-
value = str(text or "").strip()
|
|
174
|
-
if not value:
|
|
175
|
-
return ""
|
|
176
|
-
normalized = " ".join(value.split())
|
|
177
|
-
if len(normalized) <= limit:
|
|
178
|
-
return normalized
|
|
179
|
-
return normalized[: limit - 1].rstrip() + "…"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def _image_error_response(exc: Exception) -> JSONResponse:
|
|
183
|
-
from services.protocol.conversation import public_image_error_message
|
|
184
|
-
|
|
185
|
-
message = public_image_error_message(str(exc))
|
|
186
|
-
if "no available image quota" in message.lower():
|
|
187
|
-
return openai_error_response(
|
|
188
|
-
{
|
|
189
|
-
"error": {
|
|
190
|
-
"message": "no available image quota",
|
|
191
|
-
"type": "insufficient_quota",
|
|
192
|
-
"param": None,
|
|
193
|
-
"code": "insufficient_quota",
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
429,
|
|
197
|
-
)
|
|
198
|
-
if hasattr(exc, "to_openai_error") and hasattr(exc, "status_code"):
|
|
199
|
-
return JSONResponse(status_code=int(exc.status_code), content=exc.to_openai_error())
|
|
200
|
-
return openai_error_response(message, 502)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def _protocol_error_response(exc: Exception, status_code: int, sse: str) -> JSONResponse:
|
|
204
|
-
message = str(exc)
|
|
205
|
-
if sse == "anthropic":
|
|
206
|
-
return anthropic_error_response(message, status_code)
|
|
207
|
-
return openai_error_response(message, status_code)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _next_item(items):
|
|
211
|
-
try:
|
|
212
|
-
return True, next(items)
|
|
213
|
-
except StopIteration:
|
|
214
|
-
return False, None
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
@dataclass
|
|
218
|
-
class LoggedCall:
|
|
219
|
-
identity: dict[str, object]
|
|
220
|
-
endpoint: str
|
|
221
|
-
model: str
|
|
222
|
-
summary: str
|
|
223
|
-
started: float = field(default_factory=time.time)
|
|
224
|
-
request_text: str = ""
|
|
225
|
-
request_shape: dict[str, int] | None = None
|
|
226
|
-
|
|
227
|
-
async def run(self, handler, *args, sse: str = "openai"):
|
|
228
|
-
from services.protocol.conversation import ImageGenerationError
|
|
229
|
-
|
|
230
|
-
try:
|
|
231
|
-
result = await run_in_threadpool(handler, *args)
|
|
232
|
-
except ImageGenerationError as exc:
|
|
233
|
-
self.log("调用失败", status="failed", error=str(exc), account_email=getattr(exc, "account_email", ""),
|
|
234
|
-
conversation_id=getattr(exc, "conversation_id", ""))
|
|
235
|
-
return _image_error_response(exc)
|
|
236
|
-
except HTTPException as exc:
|
|
237
|
-
self.log("调用失败", status="failed", error=str(exc.detail))
|
|
238
|
-
raise
|
|
239
|
-
except Exception as exc:
|
|
240
|
-
self.log("调用失败", status="failed", error=str(exc), account_email=getattr(exc, "account_email", ""))
|
|
241
|
-
if self.endpoint.startswith("/v1/images"):
|
|
242
|
-
return _image_error_response(exc)
|
|
243
|
-
return _protocol_error_response(exc, 502, sse)
|
|
244
|
-
|
|
245
|
-
if isinstance(result, dict):
|
|
246
|
-
self.log("调用完成", result)
|
|
247
|
-
response = dict(result)
|
|
248
|
-
response.pop("_account_email", None)
|
|
249
|
-
return response
|
|
250
|
-
|
|
251
|
-
sender = anthropic_sse_stream if sse == "anthropic" else sse_json_stream
|
|
252
|
-
try:
|
|
253
|
-
has_first, first = await run_in_threadpool(_next_item, result)
|
|
254
|
-
except ImageGenerationError as exc:
|
|
255
|
-
self.log("调用失败", status="failed", error=str(exc), account_email=getattr(exc, "account_email", ""),
|
|
256
|
-
conversation_id=getattr(exc, "conversation_id", ""))
|
|
257
|
-
return _image_error_response(exc)
|
|
258
|
-
except HTTPException as exc:
|
|
259
|
-
self.log("调用失败", status="failed", error=str(exc.detail))
|
|
260
|
-
raise
|
|
261
|
-
except Exception as exc:
|
|
262
|
-
self.log("调用失败", status="failed", error=str(exc), account_email=getattr(exc, "account_email", ""))
|
|
263
|
-
if self.endpoint.startswith("/v1/images"):
|
|
264
|
-
return _image_error_response(exc)
|
|
265
|
-
return _protocol_error_response(exc, 502, sse)
|
|
266
|
-
if not has_first:
|
|
267
|
-
self.log("流式调用结束")
|
|
268
|
-
return StreamingResponse(sender(()), media_type="text/event-stream")
|
|
269
|
-
return StreamingResponse(sender(self.stream(itertools.chain([first], result))), media_type="text/event-stream")
|
|
270
|
-
|
|
271
|
-
def stream(self, items):
|
|
272
|
-
urls: list[str] = []
|
|
273
|
-
account_emails: list[str] = []
|
|
274
|
-
conversation_ids: list[str] = []
|
|
275
|
-
failed = False
|
|
276
|
-
try:
|
|
277
|
-
for item in items:
|
|
278
|
-
urls.extend(_collect_urls(item))
|
|
279
|
-
account_emails.extend(_collect_account_emails(item))
|
|
280
|
-
conversation_ids.extend(_collect_conversation_ids(item))
|
|
281
|
-
yield _strip_internal_response_fields(item)
|
|
282
|
-
except Exception as exc:
|
|
283
|
-
failed = True
|
|
284
|
-
self.log(
|
|
285
|
-
"流式调用失败",
|
|
286
|
-
status="failed",
|
|
287
|
-
error=str(exc),
|
|
288
|
-
urls=urls,
|
|
289
|
-
account_email=(account_emails[0] if account_emails else getattr(exc, "account_email", "")),
|
|
290
|
-
conversation_id=(conversation_ids[0] if conversation_ids else getattr(exc, "conversation_id", "")),
|
|
291
|
-
)
|
|
292
|
-
if self.endpoint.startswith("/v1/images") and not hasattr(exc, "to_openai_error"):
|
|
293
|
-
from services.protocol.conversation import ImageGenerationError, public_image_error_message
|
|
294
|
-
|
|
295
|
-
raise ImageGenerationError(public_image_error_message(str(exc))) from exc
|
|
296
|
-
raise
|
|
297
|
-
finally:
|
|
298
|
-
if not failed:
|
|
299
|
-
self.log("流式调用结束", urls=urls, account_email=account_emails[0] if account_emails else "",
|
|
300
|
-
conversation_id=conversation_ids[0] if conversation_ids else "")
|
|
301
|
-
|
|
302
|
-
def log(self, suffix: str, result: object = None, status: str = "success", error: str = "",
|
|
303
|
-
urls: list[str] | None = None, account_email: str = "", conversation_id: str = "") -> None:
|
|
304
|
-
detail = {
|
|
305
|
-
"key_id": self.identity.get("id"),
|
|
306
|
-
"key_name": self.identity.get("name"),
|
|
307
|
-
"role": self.identity.get("role"),
|
|
308
|
-
"endpoint": self.endpoint,
|
|
309
|
-
"model": self.model,
|
|
310
|
-
"started_at": datetime.fromtimestamp(self.started).strftime("%Y-%m-%d %H:%M:%S"),
|
|
311
|
-
"ended_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
312
|
-
"duration_ms": int((time.time() - self.started) * 1000),
|
|
313
|
-
"status": status,
|
|
314
|
-
}
|
|
315
|
-
request_excerpt = _request_excerpt(self.request_text)
|
|
316
|
-
if request_excerpt:
|
|
317
|
-
detail["request_text"] = request_excerpt
|
|
318
|
-
if self.request_shape:
|
|
319
|
-
detail["request_shape"] = self.request_shape
|
|
320
|
-
if error:
|
|
321
|
-
detail["error"] = error
|
|
322
|
-
email = str(account_email or "").strip()
|
|
323
|
-
if not email:
|
|
324
|
-
emails = _collect_account_emails(result)
|
|
325
|
-
email = emails[0] if emails else ""
|
|
326
|
-
if email:
|
|
327
|
-
detail["account_email"] = email
|
|
328
|
-
conv_id = str(conversation_id or "").strip()
|
|
329
|
-
if not conv_id:
|
|
330
|
-
conv_ids = _collect_conversation_ids(result)
|
|
331
|
-
conv_id = conv_ids[0] if conv_ids else ""
|
|
332
|
-
if conv_id:
|
|
333
|
-
detail["conversation_id"] = conv_id
|
|
334
|
-
collected_urls = [*(urls or []), *_collect_urls(result)]
|
|
335
|
-
if collected_urls and not self.endpoint.startswith("/v1/search"):
|
|
336
|
-
detail["urls"] = list(dict.fromkeys(collected_urls))
|
|
337
|
-
log_service.add(LOG_TYPE_CALL, f"{self.summary}{suffix}", detail)
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import itertools
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from fastapi import HTTPException
|
|
14
|
+
from fastapi.concurrency import run_in_threadpool
|
|
15
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
16
|
+
|
|
17
|
+
from services.config import DATA_DIR
|
|
18
|
+
from services.protocol.error_response import anthropic_error_response, openai_error_response
|
|
19
|
+
from utils.helper import anthropic_sse_stream, sse_json_stream
|
|
20
|
+
|
|
21
|
+
LOG_TYPE_CALL = "call"
|
|
22
|
+
LOG_TYPE_ACCOUNT = "account"
|
|
23
|
+
INTERNAL_RESPONSE_KEYS = {"_account_email", "_conversation_id"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LogService:
|
|
27
|
+
def __init__(self, path: Path):
|
|
28
|
+
self.path = path
|
|
29
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _legacy_id(raw_line: str, line_number: int) -> str:
|
|
33
|
+
payload = f"{line_number}:{raw_line}".encode("utf-8", errors="ignore")
|
|
34
|
+
return hashlib.sha1(payload).hexdigest()[:24]
|
|
35
|
+
|
|
36
|
+
def _parse_line(self, raw_line: str, line_number: int) -> dict[str, Any] | None:
|
|
37
|
+
try:
|
|
38
|
+
item = json.loads(raw_line)
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
if not isinstance(item, dict):
|
|
42
|
+
return None
|
|
43
|
+
parsed = dict(item)
|
|
44
|
+
parsed["id"] = str(parsed.get("id") or self._legacy_id(raw_line, line_number))
|
|
45
|
+
return parsed
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _serialize_item(item: dict[str, Any]) -> str:
|
|
49
|
+
return json.dumps(item, ensure_ascii=False, separators=(",", ":"))
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _matches_filters(item: dict[str, Any], *, type: str = "", start_date: str = "", end_date: str = "") -> bool:
|
|
53
|
+
t = str(item.get("time") or "")
|
|
54
|
+
day = t[:10]
|
|
55
|
+
if type and item.get("type") != type:
|
|
56
|
+
return False
|
|
57
|
+
if start_date and day < start_date:
|
|
58
|
+
return False
|
|
59
|
+
if end_date and day > end_date:
|
|
60
|
+
return False
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
def add(self, type: str, summary: str = "", detail: dict[str, Any] | None = None, **data: Any) -> None:
|
|
64
|
+
item = {
|
|
65
|
+
"id": uuid4().hex,
|
|
66
|
+
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
67
|
+
"type": type,
|
|
68
|
+
"summary": summary,
|
|
69
|
+
"detail": detail or data,
|
|
70
|
+
}
|
|
71
|
+
with self.path.open("a", encoding="utf-8") as file:
|
|
72
|
+
file.write(self._serialize_item(item) + "\n")
|
|
73
|
+
|
|
74
|
+
def list(self, type: str = "", start_date: str = "", end_date: str = "", limit: int = 200) -> list[dict[str, Any]]:
|
|
75
|
+
if not self.path.exists():
|
|
76
|
+
return []
|
|
77
|
+
items: list[dict[str, Any]] = []
|
|
78
|
+
lines = self.path.read_text(encoding="utf-8").splitlines()
|
|
79
|
+
for line_number in range(len(lines) - 1, -1, -1):
|
|
80
|
+
item = self._parse_line(lines[line_number], line_number)
|
|
81
|
+
if item is None:
|
|
82
|
+
continue
|
|
83
|
+
if not self._matches_filters(item, type=type, start_date=start_date, end_date=end_date):
|
|
84
|
+
continue
|
|
85
|
+
items.append(item)
|
|
86
|
+
if len(items) >= limit:
|
|
87
|
+
break
|
|
88
|
+
return items
|
|
89
|
+
|
|
90
|
+
def delete(self, ids: list[str]) -> dict[str, int]:
|
|
91
|
+
target_ids = {str(item or "").strip() for item in ids if str(item or "").strip()}
|
|
92
|
+
if not self.path.exists() or not target_ids:
|
|
93
|
+
return {"removed": 0}
|
|
94
|
+
lines = self.path.read_text(encoding="utf-8").splitlines()
|
|
95
|
+
kept_lines: list[str] = []
|
|
96
|
+
removed = 0
|
|
97
|
+
for line_number, raw_line in enumerate(lines):
|
|
98
|
+
item = self._parse_line(raw_line, line_number)
|
|
99
|
+
if item is None:
|
|
100
|
+
kept_lines.append(raw_line)
|
|
101
|
+
continue
|
|
102
|
+
if str(item.get("id") or "") in target_ids:
|
|
103
|
+
removed += 1
|
|
104
|
+
continue
|
|
105
|
+
kept_lines.append(self._serialize_item(item))
|
|
106
|
+
content = "\n".join(kept_lines)
|
|
107
|
+
if content:
|
|
108
|
+
content += "\n"
|
|
109
|
+
self.path.write_text(content, encoding="utf-8")
|
|
110
|
+
return {"removed": removed}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
log_service = LogService(DATA_DIR / "logs.jsonl")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _collect_urls(value: object) -> list[str]:
|
|
117
|
+
urls: list[str] = []
|
|
118
|
+
if isinstance(value, dict):
|
|
119
|
+
for key, item in value.items():
|
|
120
|
+
if key == "url" and isinstance(item, str):
|
|
121
|
+
urls.append(item)
|
|
122
|
+
elif key == "urls" and isinstance(item, list):
|
|
123
|
+
urls.extend(str(url) for url in item if isinstance(url, str))
|
|
124
|
+
else:
|
|
125
|
+
urls.extend(_collect_urls(item))
|
|
126
|
+
elif isinstance(value, list):
|
|
127
|
+
for item in value:
|
|
128
|
+
urls.extend(_collect_urls(item))
|
|
129
|
+
return urls
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _collect_account_emails(value: object) -> list[str]:
|
|
133
|
+
emails: list[str] = []
|
|
134
|
+
if isinstance(value, dict):
|
|
135
|
+
for key, item in value.items():
|
|
136
|
+
if key in {"_account_email", "account_email"} and isinstance(item, str) and item.strip():
|
|
137
|
+
emails.append(item.strip())
|
|
138
|
+
else:
|
|
139
|
+
emails.extend(_collect_account_emails(item))
|
|
140
|
+
elif isinstance(value, list):
|
|
141
|
+
for item in value:
|
|
142
|
+
emails.extend(_collect_account_emails(item))
|
|
143
|
+
return emails
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _collect_conversation_ids(value: object) -> list[str]:
|
|
147
|
+
ids: list[str] = []
|
|
148
|
+
if isinstance(value, dict):
|
|
149
|
+
for key, item in value.items():
|
|
150
|
+
if key == "_conversation_id" and isinstance(item, str) and item.strip():
|
|
151
|
+
ids.append(item.strip())
|
|
152
|
+
else:
|
|
153
|
+
ids.extend(_collect_conversation_ids(item))
|
|
154
|
+
elif isinstance(value, list):
|
|
155
|
+
for item in value:
|
|
156
|
+
ids.extend(_collect_conversation_ids(item))
|
|
157
|
+
return ids
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _strip_internal_response_fields(value: object) -> object:
|
|
161
|
+
if isinstance(value, dict):
|
|
162
|
+
return {
|
|
163
|
+
key: _strip_internal_response_fields(item)
|
|
164
|
+
for key, item in value.items()
|
|
165
|
+
if key not in INTERNAL_RESPONSE_KEYS
|
|
166
|
+
}
|
|
167
|
+
if isinstance(value, list):
|
|
168
|
+
return [_strip_internal_response_fields(item) for item in value]
|
|
169
|
+
return value
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _request_excerpt(text: object, limit: int = 1000) -> str:
|
|
173
|
+
value = str(text or "").strip()
|
|
174
|
+
if not value:
|
|
175
|
+
return ""
|
|
176
|
+
normalized = " ".join(value.split())
|
|
177
|
+
if len(normalized) <= limit:
|
|
178
|
+
return normalized
|
|
179
|
+
return normalized[: limit - 1].rstrip() + "…"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _image_error_response(exc: Exception) -> JSONResponse:
|
|
183
|
+
from services.protocol.conversation import public_image_error_message
|
|
184
|
+
|
|
185
|
+
message = public_image_error_message(str(exc))
|
|
186
|
+
if "no available image quota" in message.lower():
|
|
187
|
+
return openai_error_response(
|
|
188
|
+
{
|
|
189
|
+
"error": {
|
|
190
|
+
"message": "no available image quota",
|
|
191
|
+
"type": "insufficient_quota",
|
|
192
|
+
"param": None,
|
|
193
|
+
"code": "insufficient_quota",
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
429,
|
|
197
|
+
)
|
|
198
|
+
if hasattr(exc, "to_openai_error") and hasattr(exc, "status_code"):
|
|
199
|
+
return JSONResponse(status_code=int(exc.status_code), content=exc.to_openai_error())
|
|
200
|
+
return openai_error_response(message, 502)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _protocol_error_response(exc: Exception, status_code: int, sse: str) -> JSONResponse:
|
|
204
|
+
message = str(exc)
|
|
205
|
+
if sse == "anthropic":
|
|
206
|
+
return anthropic_error_response(message, status_code)
|
|
207
|
+
return openai_error_response(message, status_code)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _next_item(items):
|
|
211
|
+
try:
|
|
212
|
+
return True, next(items)
|
|
213
|
+
except StopIteration:
|
|
214
|
+
return False, None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class LoggedCall:
|
|
219
|
+
identity: dict[str, object]
|
|
220
|
+
endpoint: str
|
|
221
|
+
model: str
|
|
222
|
+
summary: str
|
|
223
|
+
started: float = field(default_factory=time.time)
|
|
224
|
+
request_text: str = ""
|
|
225
|
+
request_shape: dict[str, int] | None = None
|
|
226
|
+
|
|
227
|
+
async def run(self, handler, *args, sse: str = "openai"):
|
|
228
|
+
from services.protocol.conversation import ImageGenerationError
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
result = await run_in_threadpool(handler, *args)
|
|
232
|
+
except ImageGenerationError as exc:
|
|
233
|
+
self.log("调用失败", status="failed", error=str(exc), account_email=getattr(exc, "account_email", ""),
|
|
234
|
+
conversation_id=getattr(exc, "conversation_id", ""))
|
|
235
|
+
return _image_error_response(exc)
|
|
236
|
+
except HTTPException as exc:
|
|
237
|
+
self.log("调用失败", status="failed", error=str(exc.detail))
|
|
238
|
+
raise
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
self.log("调用失败", status="failed", error=str(exc), account_email=getattr(exc, "account_email", ""))
|
|
241
|
+
if self.endpoint.startswith("/v1/images"):
|
|
242
|
+
return _image_error_response(exc)
|
|
243
|
+
return _protocol_error_response(exc, 502, sse)
|
|
244
|
+
|
|
245
|
+
if isinstance(result, dict):
|
|
246
|
+
self.log("调用完成", result)
|
|
247
|
+
response = dict(result)
|
|
248
|
+
response.pop("_account_email", None)
|
|
249
|
+
return response
|
|
250
|
+
|
|
251
|
+
sender = anthropic_sse_stream if sse == "anthropic" else sse_json_stream
|
|
252
|
+
try:
|
|
253
|
+
has_first, first = await run_in_threadpool(_next_item, result)
|
|
254
|
+
except ImageGenerationError as exc:
|
|
255
|
+
self.log("调用失败", status="failed", error=str(exc), account_email=getattr(exc, "account_email", ""),
|
|
256
|
+
conversation_id=getattr(exc, "conversation_id", ""))
|
|
257
|
+
return _image_error_response(exc)
|
|
258
|
+
except HTTPException as exc:
|
|
259
|
+
self.log("调用失败", status="failed", error=str(exc.detail))
|
|
260
|
+
raise
|
|
261
|
+
except Exception as exc:
|
|
262
|
+
self.log("调用失败", status="failed", error=str(exc), account_email=getattr(exc, "account_email", ""))
|
|
263
|
+
if self.endpoint.startswith("/v1/images"):
|
|
264
|
+
return _image_error_response(exc)
|
|
265
|
+
return _protocol_error_response(exc, 502, sse)
|
|
266
|
+
if not has_first:
|
|
267
|
+
self.log("流式调用结束")
|
|
268
|
+
return StreamingResponse(sender(()), media_type="text/event-stream")
|
|
269
|
+
return StreamingResponse(sender(self.stream(itertools.chain([first], result))), media_type="text/event-stream")
|
|
270
|
+
|
|
271
|
+
def stream(self, items):
|
|
272
|
+
urls: list[str] = []
|
|
273
|
+
account_emails: list[str] = []
|
|
274
|
+
conversation_ids: list[str] = []
|
|
275
|
+
failed = False
|
|
276
|
+
try:
|
|
277
|
+
for item in items:
|
|
278
|
+
urls.extend(_collect_urls(item))
|
|
279
|
+
account_emails.extend(_collect_account_emails(item))
|
|
280
|
+
conversation_ids.extend(_collect_conversation_ids(item))
|
|
281
|
+
yield _strip_internal_response_fields(item)
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
failed = True
|
|
284
|
+
self.log(
|
|
285
|
+
"流式调用失败",
|
|
286
|
+
status="failed",
|
|
287
|
+
error=str(exc),
|
|
288
|
+
urls=urls,
|
|
289
|
+
account_email=(account_emails[0] if account_emails else getattr(exc, "account_email", "")),
|
|
290
|
+
conversation_id=(conversation_ids[0] if conversation_ids else getattr(exc, "conversation_id", "")),
|
|
291
|
+
)
|
|
292
|
+
if self.endpoint.startswith("/v1/images") and not hasattr(exc, "to_openai_error"):
|
|
293
|
+
from services.protocol.conversation import ImageGenerationError, public_image_error_message
|
|
294
|
+
|
|
295
|
+
raise ImageGenerationError(public_image_error_message(str(exc))) from exc
|
|
296
|
+
raise
|
|
297
|
+
finally:
|
|
298
|
+
if not failed:
|
|
299
|
+
self.log("流式调用结束", urls=urls, account_email=account_emails[0] if account_emails else "",
|
|
300
|
+
conversation_id=conversation_ids[0] if conversation_ids else "")
|
|
301
|
+
|
|
302
|
+
def log(self, suffix: str, result: object = None, status: str = "success", error: str = "",
|
|
303
|
+
urls: list[str] | None = None, account_email: str = "", conversation_id: str = "") -> None:
|
|
304
|
+
detail = {
|
|
305
|
+
"key_id": self.identity.get("id"),
|
|
306
|
+
"key_name": self.identity.get("name"),
|
|
307
|
+
"role": self.identity.get("role"),
|
|
308
|
+
"endpoint": self.endpoint,
|
|
309
|
+
"model": self.model,
|
|
310
|
+
"started_at": datetime.fromtimestamp(self.started).strftime("%Y-%m-%d %H:%M:%S"),
|
|
311
|
+
"ended_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
312
|
+
"duration_ms": int((time.time() - self.started) * 1000),
|
|
313
|
+
"status": status,
|
|
314
|
+
}
|
|
315
|
+
request_excerpt = _request_excerpt(self.request_text)
|
|
316
|
+
if request_excerpt:
|
|
317
|
+
detail["request_text"] = request_excerpt
|
|
318
|
+
if self.request_shape:
|
|
319
|
+
detail["request_shape"] = self.request_shape
|
|
320
|
+
if error:
|
|
321
|
+
detail["error"] = error
|
|
322
|
+
email = str(account_email or "").strip()
|
|
323
|
+
if not email:
|
|
324
|
+
emails = _collect_account_emails(result)
|
|
325
|
+
email = emails[0] if emails else ""
|
|
326
|
+
if email:
|
|
327
|
+
detail["account_email"] = email
|
|
328
|
+
conv_id = str(conversation_id or "").strip()
|
|
329
|
+
if not conv_id:
|
|
330
|
+
conv_ids = _collect_conversation_ids(result)
|
|
331
|
+
conv_id = conv_ids[0] if conv_ids else ""
|
|
332
|
+
if conv_id:
|
|
333
|
+
detail["conversation_id"] = conv_id
|
|
334
|
+
collected_urls = [*(urls or []), *_collect_urls(result)]
|
|
335
|
+
if collected_urls and not self.endpoint.startswith("/v1/search"):
|
|
336
|
+
detail["urls"] = list(dict.fromkeys(collected_urls))
|
|
337
|
+
log_service.add(LOG_TYPE_CALL, f"{self.summary}{suffix}", detail)
|