botrun-flow-lang 5.12.263__py3-none-any.whl → 6.2.21__py3-none-any.whl
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.
- botrun_flow_lang/api/auth_api.py +39 -39
- botrun_flow_lang/api/auth_utils.py +183 -183
- botrun_flow_lang/api/botrun_back_api.py +65 -65
- botrun_flow_lang/api/flow_api.py +3 -3
- botrun_flow_lang/api/hatch_api.py +508 -508
- botrun_flow_lang/api/langgraph_api.py +816 -811
- botrun_flow_lang/api/langgraph_constants.py +11 -0
- botrun_flow_lang/api/line_bot_api.py +1484 -1484
- botrun_flow_lang/api/model_api.py +300 -300
- botrun_flow_lang/api/rate_limit_api.py +32 -32
- botrun_flow_lang/api/routes.py +79 -79
- botrun_flow_lang/api/search_api.py +53 -53
- botrun_flow_lang/api/storage_api.py +395 -395
- botrun_flow_lang/api/subsidy_api.py +290 -290
- botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
- botrun_flow_lang/api/user_setting_api.py +70 -70
- botrun_flow_lang/api/version_api.py +31 -31
- botrun_flow_lang/api/youtube_api.py +26 -26
- botrun_flow_lang/constants.py +13 -13
- botrun_flow_lang/langgraph_agents/agents/agent_runner.py +178 -178
- botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
- botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gemini_subsidy_graph.py +460 -460
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
- botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +730 -723
- botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
- botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
- botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
- botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
- botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
- botrun_flow_lang/langgraph_agents/agents/util/img_util.py +336 -294
- botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
- botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
- botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
- botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +562 -486
- botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
- botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
- botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
- botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
- botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
- botrun_flow_lang/langgraph_agents/agents/util/usage_metadata.py +34 -0
- botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
- botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
- botrun_flow_lang/llm_agent/llm_agent.py +19 -19
- botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
- botrun_flow_lang/log/.gitignore +2 -2
- botrun_flow_lang/main.py +61 -61
- botrun_flow_lang/main_fast.py +51 -51
- botrun_flow_lang/mcp_server/__init__.py +10 -10
- botrun_flow_lang/mcp_server/default_mcp.py +854 -744
- botrun_flow_lang/models/nodes/utils.py +205 -205
- botrun_flow_lang/models/token_usage.py +34 -34
- botrun_flow_lang/requirements.txt +21 -21
- botrun_flow_lang/services/base/firestore_base.py +30 -30
- botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
- botrun_flow_lang/services/hatch/hatch_fs_store.py +419 -419
- botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
- botrun_flow_lang/services/storage/storage_factory.py +12 -12
- botrun_flow_lang/services/storage/storage_store.py +65 -65
- botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
- botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
- botrun_flow_lang/static/docs/tools/index.html +926 -926
- botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
- botrun_flow_lang/tests/api_stress_test.py +357 -357
- botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
- botrun_flow_lang/tests/test_botrun_app.py +46 -46
- botrun_flow_lang/tests/test_html_util.py +31 -31
- botrun_flow_lang/tests/test_img_analyzer.py +190 -190
- botrun_flow_lang/tests/test_img_util.py +39 -39
- botrun_flow_lang/tests/test_local_files.py +114 -114
- botrun_flow_lang/tests/test_mermaid_util.py +103 -103
- botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
- botrun_flow_lang/tests/test_plotly_util.py +151 -151
- botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
- botrun_flow_lang/tools/generate_docs.py +133 -133
- botrun_flow_lang/tools/templates/tools.html +153 -153
- botrun_flow_lang/utils/__init__.py +7 -7
- botrun_flow_lang/utils/botrun_logger.py +344 -344
- botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
- botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
- botrun_flow_lang/utils/google_drive_utils.py +654 -654
- botrun_flow_lang/utils/langchain_utils.py +324 -324
- botrun_flow_lang/utils/yaml_utils.py +9 -9
- {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-6.2.21.dist-info}/METADATA +6 -6
- botrun_flow_lang-6.2.21.dist-info/RECORD +104 -0
- botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
- {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-6.2.21.dist-info}/WHEEL +0 -0
|
@@ -1,419 +1,419 @@
|
|
|
1
|
-
import requests
|
|
2
|
-
import mimetypes
|
|
3
|
-
import time
|
|
4
|
-
import random
|
|
5
|
-
import os
|
|
6
|
-
from tempfile import NamedTemporaryFile
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from io import BytesIO
|
|
9
|
-
from .img_util import get_img_content_type
|
|
10
|
-
from botrun_flow_lang.services.storage.storage_factory import storage_store_factory
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_file_content_type(file_path: str) -> str:
|
|
14
|
-
"""根據檔案類型取得對應的 MIME type
|
|
15
|
-
如果是圖片檔案,會使用 get_img_content_type 來檢測實際的圖片格式
|
|
16
|
-
如果是其他檔案,則使用副檔名來判斷
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
file_path: 檔案路徑
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
str: MIME type,如果無法判斷則返回 application/octet-stream
|
|
23
|
-
"""
|
|
24
|
-
# 先用副檔名判斷是否為圖片
|
|
25
|
-
content_type, _ = mimetypes.guess_type(file_path)
|
|
26
|
-
|
|
27
|
-
# 如果副檔名顯示是圖片,使用 get_img_content_type 進行實際檢測
|
|
28
|
-
if content_type and content_type.startswith("image/"):
|
|
29
|
-
try:
|
|
30
|
-
return get_img_content_type(file_path)
|
|
31
|
-
except (ValueError, FileNotFoundError):
|
|
32
|
-
# 如果圖片檢測失敗,回傳原本用副檔名判斷的結果
|
|
33
|
-
return content_type
|
|
34
|
-
|
|
35
|
-
# 非圖片檔案或無法判斷類型,回傳原本的結果
|
|
36
|
-
return content_type or "application/octet-stream"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def upload_and_get_tmp_public_url(
|
|
40
|
-
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
41
|
-
) -> str:
|
|
42
|
-
"""上傳檔案到 GCS,並取得公開存取的 URL
|
|
43
|
-
如果是圖片檔案,會根據實際的圖片格式來調整檔案副檔名
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
file_path: 本地檔案路徑
|
|
47
|
-
botrun_flow_lang_url: botrun flow lang API 的 URL,預設為空字串
|
|
48
|
-
user_id: 使用者 ID
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
str: 上傳後的公開存取 URL
|
|
52
|
-
"""
|
|
53
|
-
# 外層 try-except: 處理第一次嘗試與重試邏輯
|
|
54
|
-
try:
|
|
55
|
-
return _perform_tmp_file_upload(file_path, botrun_flow_lang_url, user_id)
|
|
56
|
-
except Exception as e:
|
|
57
|
-
import traceback
|
|
58
|
-
|
|
59
|
-
# 第一次嘗試失敗,記錄錯誤但不立即返回
|
|
60
|
-
print(f"First attempt failed: {str(e)}")
|
|
61
|
-
traceback.print_exc()
|
|
62
|
-
|
|
63
|
-
# 隨機等待 7-15 秒
|
|
64
|
-
retry_delay = random.randint(7, 20)
|
|
65
|
-
print(f"Retrying in {retry_delay} seconds...")
|
|
66
|
-
time.sleep(retry_delay)
|
|
67
|
-
|
|
68
|
-
# 第二次嘗試
|
|
69
|
-
try:
|
|
70
|
-
print("Retry attempt...")
|
|
71
|
-
return _perform_tmp_file_upload(file_path, botrun_flow_lang_url, user_id)
|
|
72
|
-
except Exception as retry_e:
|
|
73
|
-
# 第二次嘗試也失敗,記錄錯誤並返回錯誤訊息
|
|
74
|
-
print(f"Retry attempt failed: {str(retry_e)}")
|
|
75
|
-
traceback.print_exc()
|
|
76
|
-
return "Error uploading file"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _perform_tmp_file_upload(
|
|
80
|
-
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
81
|
-
) -> str:
|
|
82
|
-
"""執行實際的上傳操作
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
file_path: 本地檔案路徑
|
|
86
|
-
botrun_flow_lang_url: botrun flow lang API 的 URL
|
|
87
|
-
user_id: 使用者 ID
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
str: 上傳後的公開存取 URL
|
|
91
|
-
"""
|
|
92
|
-
try:
|
|
93
|
-
# 如果沒有提供 API URL,使用預設值
|
|
94
|
-
if not botrun_flow_lang_url or not user_id:
|
|
95
|
-
raise ValueError("botrun_flow_lang_url and user_id are required")
|
|
96
|
-
|
|
97
|
-
# 取得檔案的 MIME type
|
|
98
|
-
content_type = get_file_content_type(file_path)
|
|
99
|
-
|
|
100
|
-
# 從檔案路徑取得檔案名稱
|
|
101
|
-
file_name = Path(file_path).name
|
|
102
|
-
|
|
103
|
-
# 如果是圖片檔案,根據實際的 content type 調整副檔名
|
|
104
|
-
if content_type.startswith("image/"):
|
|
105
|
-
# 取得檔案名稱(不含副檔名)
|
|
106
|
-
name_without_ext = Path(file_name).stem
|
|
107
|
-
# 根據 content type 決定新的副檔名
|
|
108
|
-
ext_map = {
|
|
109
|
-
"image/jpeg": ".jpg",
|
|
110
|
-
"image/png": ".png",
|
|
111
|
-
"image/gif": ".gif",
|
|
112
|
-
"image/webp": ".webp",
|
|
113
|
-
}
|
|
114
|
-
new_ext = ext_map.get(content_type, Path(file_name).suffix)
|
|
115
|
-
file_name = f"{name_without_ext}{new_ext}"
|
|
116
|
-
|
|
117
|
-
# 準備 API endpoint
|
|
118
|
-
url = f"{botrun_flow_lang_url}/api/tmp-files/{user_id}"
|
|
119
|
-
|
|
120
|
-
# 準備檔案
|
|
121
|
-
files = {
|
|
122
|
-
"file": (file_name, open(file_path, "rb"), content_type),
|
|
123
|
-
"file_name": (None, file_name),
|
|
124
|
-
"content_type": (None, content_type),
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
# 發送請求
|
|
128
|
-
response = requests.post(url, files=files)
|
|
129
|
-
response.raise_for_status() # 如果請求失敗會拋出異常
|
|
130
|
-
|
|
131
|
-
# 從回應中取得 URL
|
|
132
|
-
result = response.json()
|
|
133
|
-
return result.get("url", "")
|
|
134
|
-
|
|
135
|
-
except Exception as e:
|
|
136
|
-
# 這裡把異常往上拋,讓外層的重試邏輯處理
|
|
137
|
-
raise e
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _upload_text_file_with_utf8(
|
|
141
|
-
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
142
|
-
) -> str:
|
|
143
|
-
"""上傳文字檔案到 GCS,並設定正確的 UTF-8 charset
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
file_path: 本地檔案路徑
|
|
147
|
-
botrun_flow_lang_url: botrun flow lang API 的 URL
|
|
148
|
-
user_id: 使用者 ID
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
str: 上傳後的公開存取 URL
|
|
152
|
-
"""
|
|
153
|
-
try:
|
|
154
|
-
# 檢查必要參數
|
|
155
|
-
if not botrun_flow_lang_url or not user_id:
|
|
156
|
-
raise ValueError("botrun_flow_lang_url and user_id are required")
|
|
157
|
-
|
|
158
|
-
# 設定正確的 MIME type with UTF-8 charset
|
|
159
|
-
content_type = "text/plain; charset=utf-8"
|
|
160
|
-
|
|
161
|
-
# 從檔案路徑取得檔案名稱
|
|
162
|
-
file_name = Path(file_path).name
|
|
163
|
-
|
|
164
|
-
# 準備 API endpoint
|
|
165
|
-
url = f"{botrun_flow_lang_url}/api/tmp-files/{user_id}"
|
|
166
|
-
|
|
167
|
-
# 準備檔案
|
|
168
|
-
files = {
|
|
169
|
-
"file": (file_name, open(file_path, "rb"), content_type),
|
|
170
|
-
"file_name": (None, file_name),
|
|
171
|
-
"content_type": (None, content_type),
|
|
172
|
-
"public": (None, False),
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
# 發送請求
|
|
176
|
-
response = requests.post(url, files=files)
|
|
177
|
-
response.raise_for_status() # 如果請求失敗會拋出異常
|
|
178
|
-
|
|
179
|
-
# 從回應中取得 URL
|
|
180
|
-
result = response.json()
|
|
181
|
-
return result.get("url", "")
|
|
182
|
-
|
|
183
|
-
except Exception as e:
|
|
184
|
-
# 這裡把異常往上拋,讓外層的重試邏輯處理
|
|
185
|
-
raise e
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
async def generate_tmp_text_file(text_content: str) -> str:
|
|
189
|
-
"""
|
|
190
|
-
Generate a temporary text file from content and upload it to GCS.
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
text_content: Text content to write to the file
|
|
194
|
-
user_id: User ID for file upload
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
str: Storage path for the text file or error message starting with "Error: "
|
|
198
|
-
"""
|
|
199
|
-
try:
|
|
200
|
-
# Generate a unique filename
|
|
201
|
-
import uuid
|
|
202
|
-
|
|
203
|
-
file_name = f"tmp_{uuid.uuid4().hex[:8]}.txt"
|
|
204
|
-
|
|
205
|
-
# Create file object from text content
|
|
206
|
-
file_content_bytes = text_content.encode("utf-8")
|
|
207
|
-
file_object = BytesIO(file_content_bytes)
|
|
208
|
-
|
|
209
|
-
# Set content type
|
|
210
|
-
content_type = "text/plain; charset=utf-8"
|
|
211
|
-
|
|
212
|
-
# Build storage path
|
|
213
|
-
storage_path = f"tmp/{file_name}"
|
|
214
|
-
|
|
215
|
-
# Store file to GCS
|
|
216
|
-
storage = storage_store_factory()
|
|
217
|
-
success, _ = await storage.store_file(
|
|
218
|
-
storage_path, file_object, public=False, content_type=content_type
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
if not success:
|
|
222
|
-
return "Error: Failed to store file"
|
|
223
|
-
|
|
224
|
-
return storage_path
|
|
225
|
-
|
|
226
|
-
except Exception as e:
|
|
227
|
-
return f"Error: {str(e)}"
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
async def read_tmp_text_file(storage_path: str) -> str:
|
|
231
|
-
"""
|
|
232
|
-
Read a temporary text file from GCS storage.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
storage_path: Storage path of the file in GCS (e.g., "tmp/user_id/filename.txt")
|
|
236
|
-
|
|
237
|
-
Returns:
|
|
238
|
-
str: Text content of the file or error message starting with "Error: "
|
|
239
|
-
"""
|
|
240
|
-
try:
|
|
241
|
-
storage = storage_store_factory()
|
|
242
|
-
file_object = await storage.retrieve_file(storage_path)
|
|
243
|
-
|
|
244
|
-
if not file_object:
|
|
245
|
-
return "Error: File not found"
|
|
246
|
-
|
|
247
|
-
# Decode the file content as UTF-8 text
|
|
248
|
-
text_content = file_object.getvalue().decode("utf-8")
|
|
249
|
-
return text_content
|
|
250
|
-
|
|
251
|
-
except Exception as e:
|
|
252
|
-
return f"Error: {str(e)}"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
async def upload_html_and_get_public_url(
|
|
256
|
-
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
257
|
-
) -> str:
|
|
258
|
-
"""上傳 HTML 檔案到 GCS,並取得公開存取的 URL
|
|
259
|
-
此函數僅接受 .html 檔案
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
file_path: 本地 HTML 檔案路徑
|
|
263
|
-
botrun_flow_lang_url: botrun flow lang API 的 URL,預設為空字串
|
|
264
|
-
user_id: 使用者 ID
|
|
265
|
-
|
|
266
|
-
Returns:
|
|
267
|
-
str: 上傳後的公開存取 URL
|
|
268
|
-
"""
|
|
269
|
-
# 檢查檔案是否為 HTML 檔案
|
|
270
|
-
if not file_path.lower().endswith(".html"):
|
|
271
|
-
raise ValueError("Only HTML files are allowed")
|
|
272
|
-
|
|
273
|
-
# 外層 try-except: 處理第一次嘗試與重試邏輯
|
|
274
|
-
try:
|
|
275
|
-
return await _perform_html_upload(file_path, botrun_flow_lang_url, user_id)
|
|
276
|
-
except Exception as e:
|
|
277
|
-
import traceback
|
|
278
|
-
|
|
279
|
-
# 第一次嘗試失敗,記錄錯誤但不立即返回
|
|
280
|
-
print(f"First attempt failed: {str(e)}")
|
|
281
|
-
traceback.print_exc()
|
|
282
|
-
|
|
283
|
-
# 隨機等待 7-15 秒
|
|
284
|
-
retry_delay = random.randint(7, 20)
|
|
285
|
-
print(f"Retrying in {retry_delay} seconds...")
|
|
286
|
-
time.sleep(retry_delay)
|
|
287
|
-
|
|
288
|
-
# 第二次嘗試
|
|
289
|
-
try:
|
|
290
|
-
print("Retry attempt...")
|
|
291
|
-
return await _perform_html_upload(file_path, botrun_flow_lang_url, user_id)
|
|
292
|
-
except Exception as retry_e:
|
|
293
|
-
# 第二次嘗試也失敗,記錄錯誤並返回錯誤訊息
|
|
294
|
-
print(f"Retry attempt failed: {str(retry_e)}")
|
|
295
|
-
traceback.print_exc()
|
|
296
|
-
return "Error uploading HTML file"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
async def _perform_html_upload(
|
|
300
|
-
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
301
|
-
) -> str:
|
|
302
|
-
"""執行 HTML 檔案的上傳操作
|
|
303
|
-
|
|
304
|
-
Args:
|
|
305
|
-
file_path: 本地 HTML 檔案路徑
|
|
306
|
-
botrun_flow_lang_url: botrun flow lang API 的 URL (不再使用,保留向後相容性)
|
|
307
|
-
user_id: 使用者 ID
|
|
308
|
-
|
|
309
|
-
Returns:
|
|
310
|
-
str: 上傳後的公開存取 URL
|
|
311
|
-
"""
|
|
312
|
-
try:
|
|
313
|
-
# 檢查必要參數
|
|
314
|
-
if not user_id:
|
|
315
|
-
raise ValueError("user_id is required")
|
|
316
|
-
|
|
317
|
-
# 檢查檔案是否為 HTML 檔案
|
|
318
|
-
if not file_path.lower().endswith(".html"):
|
|
319
|
-
raise ValueError("Only HTML files are allowed")
|
|
320
|
-
|
|
321
|
-
# 取得檔案的 MIME type
|
|
322
|
-
content_type = "text/html"
|
|
323
|
-
|
|
324
|
-
# 從檔案路徑取得檔案名稱
|
|
325
|
-
file_name = Path(file_path).name
|
|
326
|
-
|
|
327
|
-
# 讀取檔案內容
|
|
328
|
-
with open(file_path, "rb") as f:
|
|
329
|
-
file_content = f.read()
|
|
330
|
-
|
|
331
|
-
# 使用內部函數直接上傳,避免 HTTP 自我呼叫
|
|
332
|
-
from botrun_flow_lang.api.storage_api import _upload_html_file_internal
|
|
333
|
-
|
|
334
|
-
# 在同步函數中運行異步函數
|
|
335
|
-
return await _upload_html_file_internal(
|
|
336
|
-
user_id, file_content, file_name, content_type
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
except Exception as e:
|
|
340
|
-
import traceback
|
|
341
|
-
|
|
342
|
-
print(f"Error uploading HTML file: {str(e)}")
|
|
343
|
-
traceback.print_exc()
|
|
344
|
-
# 這裡把異常往上拋,讓外層的重試邏輯處理
|
|
345
|
-
raise e
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
async def download_image_from_url(image_url: str) -> tuple[BytesIO, str]:
|
|
349
|
-
"""
|
|
350
|
-
從 URL 下載圖片到記憶體
|
|
351
|
-
|
|
352
|
-
Args:
|
|
353
|
-
image_url: 圖片的 URL
|
|
354
|
-
|
|
355
|
-
Returns:
|
|
356
|
-
tuple[BytesIO, str]: (圖片內容, content_type)
|
|
357
|
-
|
|
358
|
-
Raises:
|
|
359
|
-
Exception: 下載失敗時拋出例外
|
|
360
|
-
"""
|
|
361
|
-
try:
|
|
362
|
-
import httpx
|
|
363
|
-
|
|
364
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
365
|
-
response = await client.get(image_url, follow_redirects=True)
|
|
366
|
-
response.raise_for_status()
|
|
367
|
-
|
|
368
|
-
# 從 response headers 取得 content type
|
|
369
|
-
content_type = response.headers.get("content-type", "image/png")
|
|
370
|
-
|
|
371
|
-
# 將圖片內容存入 BytesIO
|
|
372
|
-
image_data = BytesIO(response.content)
|
|
373
|
-
|
|
374
|
-
return image_data, content_type
|
|
375
|
-
|
|
376
|
-
except Exception as e:
|
|
377
|
-
raise Exception(f"Failed to download image from URL: {str(e)}")
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
async def upload_image_and_get_public_url(
|
|
381
|
-
image_url: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
382
|
-
) -> str:
|
|
383
|
-
"""
|
|
384
|
-
從 URL 下載圖片並上傳到 GCS /img 目錄,取得永久公開 URL
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
image_url: 圖片來源 URL(如 DALL-E 生成的臨時 URL)
|
|
388
|
-
botrun_flow_lang_url: botrun flow lang API 的 URL
|
|
389
|
-
user_id: 使用者 ID
|
|
390
|
-
|
|
391
|
-
Returns:
|
|
392
|
-
str: 上傳後的永久公開存取 URL
|
|
393
|
-
"""
|
|
394
|
-
try:
|
|
395
|
-
# 1. 從 URL 下載圖片
|
|
396
|
-
image_data, content_type = await download_image_from_url(image_url)
|
|
397
|
-
|
|
398
|
-
# 2. 生成唯一檔案名稱
|
|
399
|
-
import uuid
|
|
400
|
-
from datetime import datetime
|
|
401
|
-
|
|
402
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
403
|
-
file_name = f"dalle_{timestamp}_{uuid.uuid4().hex[:8]}.png"
|
|
404
|
-
|
|
405
|
-
# 3. 使用內部函數上傳
|
|
406
|
-
from botrun_flow_lang.api.storage_api import _upload_img_file_internal
|
|
407
|
-
|
|
408
|
-
public_url = await _upload_img_file_internal(
|
|
409
|
-
user_id, image_data.getvalue(), file_name, content_type
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
return public_url
|
|
413
|
-
|
|
414
|
-
except Exception as e:
|
|
415
|
-
import traceback
|
|
416
|
-
|
|
417
|
-
print(f"Error uploading image: {str(e)}")
|
|
418
|
-
traceback.print_exc()
|
|
419
|
-
raise e
|
|
1
|
+
import requests
|
|
2
|
+
import mimetypes
|
|
3
|
+
import time
|
|
4
|
+
import random
|
|
5
|
+
import os
|
|
6
|
+
from tempfile import NamedTemporaryFile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from .img_util import get_img_content_type
|
|
10
|
+
from botrun_flow_lang.services.storage.storage_factory import storage_store_factory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_file_content_type(file_path: str) -> str:
|
|
14
|
+
"""根據檔案類型取得對應的 MIME type
|
|
15
|
+
如果是圖片檔案,會使用 get_img_content_type 來檢測實際的圖片格式
|
|
16
|
+
如果是其他檔案,則使用副檔名來判斷
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
file_path: 檔案路徑
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
str: MIME type,如果無法判斷則返回 application/octet-stream
|
|
23
|
+
"""
|
|
24
|
+
# 先用副檔名判斷是否為圖片
|
|
25
|
+
content_type, _ = mimetypes.guess_type(file_path)
|
|
26
|
+
|
|
27
|
+
# 如果副檔名顯示是圖片,使用 get_img_content_type 進行實際檢測
|
|
28
|
+
if content_type and content_type.startswith("image/"):
|
|
29
|
+
try:
|
|
30
|
+
return get_img_content_type(file_path)
|
|
31
|
+
except (ValueError, FileNotFoundError):
|
|
32
|
+
# 如果圖片檢測失敗,回傳原本用副檔名判斷的結果
|
|
33
|
+
return content_type
|
|
34
|
+
|
|
35
|
+
# 非圖片檔案或無法判斷類型,回傳原本的結果
|
|
36
|
+
return content_type or "application/octet-stream"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def upload_and_get_tmp_public_url(
|
|
40
|
+
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
41
|
+
) -> str:
|
|
42
|
+
"""上傳檔案到 GCS,並取得公開存取的 URL
|
|
43
|
+
如果是圖片檔案,會根據實際的圖片格式來調整檔案副檔名
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
file_path: 本地檔案路徑
|
|
47
|
+
botrun_flow_lang_url: botrun flow lang API 的 URL,預設為空字串
|
|
48
|
+
user_id: 使用者 ID
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: 上傳後的公開存取 URL
|
|
52
|
+
"""
|
|
53
|
+
# 外層 try-except: 處理第一次嘗試與重試邏輯
|
|
54
|
+
try:
|
|
55
|
+
return _perform_tmp_file_upload(file_path, botrun_flow_lang_url, user_id)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
import traceback
|
|
58
|
+
|
|
59
|
+
# 第一次嘗試失敗,記錄錯誤但不立即返回
|
|
60
|
+
print(f"First attempt failed: {str(e)}")
|
|
61
|
+
traceback.print_exc()
|
|
62
|
+
|
|
63
|
+
# 隨機等待 7-15 秒
|
|
64
|
+
retry_delay = random.randint(7, 20)
|
|
65
|
+
print(f"Retrying in {retry_delay} seconds...")
|
|
66
|
+
time.sleep(retry_delay)
|
|
67
|
+
|
|
68
|
+
# 第二次嘗試
|
|
69
|
+
try:
|
|
70
|
+
print("Retry attempt...")
|
|
71
|
+
return _perform_tmp_file_upload(file_path, botrun_flow_lang_url, user_id)
|
|
72
|
+
except Exception as retry_e:
|
|
73
|
+
# 第二次嘗試也失敗,記錄錯誤並返回錯誤訊息
|
|
74
|
+
print(f"Retry attempt failed: {str(retry_e)}")
|
|
75
|
+
traceback.print_exc()
|
|
76
|
+
return "Error uploading file"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _perform_tmp_file_upload(
|
|
80
|
+
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
81
|
+
) -> str:
|
|
82
|
+
"""執行實際的上傳操作
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
file_path: 本地檔案路徑
|
|
86
|
+
botrun_flow_lang_url: botrun flow lang API 的 URL
|
|
87
|
+
user_id: 使用者 ID
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
str: 上傳後的公開存取 URL
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
# 如果沒有提供 API URL,使用預設值
|
|
94
|
+
if not botrun_flow_lang_url or not user_id:
|
|
95
|
+
raise ValueError("botrun_flow_lang_url and user_id are required")
|
|
96
|
+
|
|
97
|
+
# 取得檔案的 MIME type
|
|
98
|
+
content_type = get_file_content_type(file_path)
|
|
99
|
+
|
|
100
|
+
# 從檔案路徑取得檔案名稱
|
|
101
|
+
file_name = Path(file_path).name
|
|
102
|
+
|
|
103
|
+
# 如果是圖片檔案,根據實際的 content type 調整副檔名
|
|
104
|
+
if content_type.startswith("image/"):
|
|
105
|
+
# 取得檔案名稱(不含副檔名)
|
|
106
|
+
name_without_ext = Path(file_name).stem
|
|
107
|
+
# 根據 content type 決定新的副檔名
|
|
108
|
+
ext_map = {
|
|
109
|
+
"image/jpeg": ".jpg",
|
|
110
|
+
"image/png": ".png",
|
|
111
|
+
"image/gif": ".gif",
|
|
112
|
+
"image/webp": ".webp",
|
|
113
|
+
}
|
|
114
|
+
new_ext = ext_map.get(content_type, Path(file_name).suffix)
|
|
115
|
+
file_name = f"{name_without_ext}{new_ext}"
|
|
116
|
+
|
|
117
|
+
# 準備 API endpoint
|
|
118
|
+
url = f"{botrun_flow_lang_url}/api/tmp-files/{user_id}"
|
|
119
|
+
|
|
120
|
+
# 準備檔案
|
|
121
|
+
files = {
|
|
122
|
+
"file": (file_name, open(file_path, "rb"), content_type),
|
|
123
|
+
"file_name": (None, file_name),
|
|
124
|
+
"content_type": (None, content_type),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# 發送請求
|
|
128
|
+
response = requests.post(url, files=files)
|
|
129
|
+
response.raise_for_status() # 如果請求失敗會拋出異常
|
|
130
|
+
|
|
131
|
+
# 從回應中取得 URL
|
|
132
|
+
result = response.json()
|
|
133
|
+
return result.get("url", "")
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
# 這裡把異常往上拋,讓外層的重試邏輯處理
|
|
137
|
+
raise e
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _upload_text_file_with_utf8(
|
|
141
|
+
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
142
|
+
) -> str:
|
|
143
|
+
"""上傳文字檔案到 GCS,並設定正確的 UTF-8 charset
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
file_path: 本地檔案路徑
|
|
147
|
+
botrun_flow_lang_url: botrun flow lang API 的 URL
|
|
148
|
+
user_id: 使用者 ID
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
str: 上傳後的公開存取 URL
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
# 檢查必要參數
|
|
155
|
+
if not botrun_flow_lang_url or not user_id:
|
|
156
|
+
raise ValueError("botrun_flow_lang_url and user_id are required")
|
|
157
|
+
|
|
158
|
+
# 設定正確的 MIME type with UTF-8 charset
|
|
159
|
+
content_type = "text/plain; charset=utf-8"
|
|
160
|
+
|
|
161
|
+
# 從檔案路徑取得檔案名稱
|
|
162
|
+
file_name = Path(file_path).name
|
|
163
|
+
|
|
164
|
+
# 準備 API endpoint
|
|
165
|
+
url = f"{botrun_flow_lang_url}/api/tmp-files/{user_id}"
|
|
166
|
+
|
|
167
|
+
# 準備檔案
|
|
168
|
+
files = {
|
|
169
|
+
"file": (file_name, open(file_path, "rb"), content_type),
|
|
170
|
+
"file_name": (None, file_name),
|
|
171
|
+
"content_type": (None, content_type),
|
|
172
|
+
"public": (None, False),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# 發送請求
|
|
176
|
+
response = requests.post(url, files=files)
|
|
177
|
+
response.raise_for_status() # 如果請求失敗會拋出異常
|
|
178
|
+
|
|
179
|
+
# 從回應中取得 URL
|
|
180
|
+
result = response.json()
|
|
181
|
+
return result.get("url", "")
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
# 這裡把異常往上拋,讓外層的重試邏輯處理
|
|
185
|
+
raise e
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def generate_tmp_text_file(text_content: str) -> str:
|
|
189
|
+
"""
|
|
190
|
+
Generate a temporary text file from content and upload it to GCS.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
text_content: Text content to write to the file
|
|
194
|
+
user_id: User ID for file upload
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
str: Storage path for the text file or error message starting with "Error: "
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
# Generate a unique filename
|
|
201
|
+
import uuid
|
|
202
|
+
|
|
203
|
+
file_name = f"tmp_{uuid.uuid4().hex[:8]}.txt"
|
|
204
|
+
|
|
205
|
+
# Create file object from text content
|
|
206
|
+
file_content_bytes = text_content.encode("utf-8")
|
|
207
|
+
file_object = BytesIO(file_content_bytes)
|
|
208
|
+
|
|
209
|
+
# Set content type
|
|
210
|
+
content_type = "text/plain; charset=utf-8"
|
|
211
|
+
|
|
212
|
+
# Build storage path
|
|
213
|
+
storage_path = f"tmp/{file_name}"
|
|
214
|
+
|
|
215
|
+
# Store file to GCS
|
|
216
|
+
storage = storage_store_factory()
|
|
217
|
+
success, _ = await storage.store_file(
|
|
218
|
+
storage_path, file_object, public=False, content_type=content_type
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if not success:
|
|
222
|
+
return "Error: Failed to store file"
|
|
223
|
+
|
|
224
|
+
return storage_path
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
return f"Error: {str(e)}"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def read_tmp_text_file(storage_path: str) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Read a temporary text file from GCS storage.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
storage_path: Storage path of the file in GCS (e.g., "tmp/user_id/filename.txt")
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
str: Text content of the file or error message starting with "Error: "
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
storage = storage_store_factory()
|
|
242
|
+
file_object = await storage.retrieve_file(storage_path)
|
|
243
|
+
|
|
244
|
+
if not file_object:
|
|
245
|
+
return "Error: File not found"
|
|
246
|
+
|
|
247
|
+
# Decode the file content as UTF-8 text
|
|
248
|
+
text_content = file_object.getvalue().decode("utf-8")
|
|
249
|
+
return text_content
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
return f"Error: {str(e)}"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def upload_html_and_get_public_url(
|
|
256
|
+
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
257
|
+
) -> str:
|
|
258
|
+
"""上傳 HTML 檔案到 GCS,並取得公開存取的 URL
|
|
259
|
+
此函數僅接受 .html 檔案
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
file_path: 本地 HTML 檔案路徑
|
|
263
|
+
botrun_flow_lang_url: botrun flow lang API 的 URL,預設為空字串
|
|
264
|
+
user_id: 使用者 ID
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
str: 上傳後的公開存取 URL
|
|
268
|
+
"""
|
|
269
|
+
# 檢查檔案是否為 HTML 檔案
|
|
270
|
+
if not file_path.lower().endswith(".html"):
|
|
271
|
+
raise ValueError("Only HTML files are allowed")
|
|
272
|
+
|
|
273
|
+
# 外層 try-except: 處理第一次嘗試與重試邏輯
|
|
274
|
+
try:
|
|
275
|
+
return await _perform_html_upload(file_path, botrun_flow_lang_url, user_id)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
import traceback
|
|
278
|
+
|
|
279
|
+
# 第一次嘗試失敗,記錄錯誤但不立即返回
|
|
280
|
+
print(f"First attempt failed: {str(e)}")
|
|
281
|
+
traceback.print_exc()
|
|
282
|
+
|
|
283
|
+
# 隨機等待 7-15 秒
|
|
284
|
+
retry_delay = random.randint(7, 20)
|
|
285
|
+
print(f"Retrying in {retry_delay} seconds...")
|
|
286
|
+
time.sleep(retry_delay)
|
|
287
|
+
|
|
288
|
+
# 第二次嘗試
|
|
289
|
+
try:
|
|
290
|
+
print("Retry attempt...")
|
|
291
|
+
return await _perform_html_upload(file_path, botrun_flow_lang_url, user_id)
|
|
292
|
+
except Exception as retry_e:
|
|
293
|
+
# 第二次嘗試也失敗,記錄錯誤並返回錯誤訊息
|
|
294
|
+
print(f"Retry attempt failed: {str(retry_e)}")
|
|
295
|
+
traceback.print_exc()
|
|
296
|
+
return "Error uploading HTML file"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def _perform_html_upload(
|
|
300
|
+
file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
301
|
+
) -> str:
|
|
302
|
+
"""執行 HTML 檔案的上傳操作
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
file_path: 本地 HTML 檔案路徑
|
|
306
|
+
botrun_flow_lang_url: botrun flow lang API 的 URL (不再使用,保留向後相容性)
|
|
307
|
+
user_id: 使用者 ID
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
str: 上傳後的公開存取 URL
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
# 檢查必要參數
|
|
314
|
+
if not user_id:
|
|
315
|
+
raise ValueError("user_id is required")
|
|
316
|
+
|
|
317
|
+
# 檢查檔案是否為 HTML 檔案
|
|
318
|
+
if not file_path.lower().endswith(".html"):
|
|
319
|
+
raise ValueError("Only HTML files are allowed")
|
|
320
|
+
|
|
321
|
+
# 取得檔案的 MIME type
|
|
322
|
+
content_type = "text/html"
|
|
323
|
+
|
|
324
|
+
# 從檔案路徑取得檔案名稱
|
|
325
|
+
file_name = Path(file_path).name
|
|
326
|
+
|
|
327
|
+
# 讀取檔案內容
|
|
328
|
+
with open(file_path, "rb") as f:
|
|
329
|
+
file_content = f.read()
|
|
330
|
+
|
|
331
|
+
# 使用內部函數直接上傳,避免 HTTP 自我呼叫
|
|
332
|
+
from botrun_flow_lang.api.storage_api import _upload_html_file_internal
|
|
333
|
+
|
|
334
|
+
# 在同步函數中運行異步函數
|
|
335
|
+
return await _upload_html_file_internal(
|
|
336
|
+
user_id, file_content, file_name, content_type
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
import traceback
|
|
341
|
+
|
|
342
|
+
print(f"Error uploading HTML file: {str(e)}")
|
|
343
|
+
traceback.print_exc()
|
|
344
|
+
# 這裡把異常往上拋,讓外層的重試邏輯處理
|
|
345
|
+
raise e
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def download_image_from_url(image_url: str) -> tuple[BytesIO, str]:
|
|
349
|
+
"""
|
|
350
|
+
從 URL 下載圖片到記憶體
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
image_url: 圖片的 URL
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
tuple[BytesIO, str]: (圖片內容, content_type)
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
Exception: 下載失敗時拋出例外
|
|
360
|
+
"""
|
|
361
|
+
try:
|
|
362
|
+
import httpx
|
|
363
|
+
|
|
364
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
365
|
+
response = await client.get(image_url, follow_redirects=True)
|
|
366
|
+
response.raise_for_status()
|
|
367
|
+
|
|
368
|
+
# 從 response headers 取得 content type
|
|
369
|
+
content_type = response.headers.get("content-type", "image/png")
|
|
370
|
+
|
|
371
|
+
# 將圖片內容存入 BytesIO
|
|
372
|
+
image_data = BytesIO(response.content)
|
|
373
|
+
|
|
374
|
+
return image_data, content_type
|
|
375
|
+
|
|
376
|
+
except Exception as e:
|
|
377
|
+
raise Exception(f"Failed to download image from URL: {str(e)}")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def upload_image_and_get_public_url(
|
|
381
|
+
image_url: str, botrun_flow_lang_url: str = "", user_id: str = ""
|
|
382
|
+
) -> str:
|
|
383
|
+
"""
|
|
384
|
+
從 URL 下載圖片並上傳到 GCS /img 目錄,取得永久公開 URL
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
image_url: 圖片來源 URL(如 DALL-E 生成的臨時 URL)
|
|
388
|
+
botrun_flow_lang_url: botrun flow lang API 的 URL
|
|
389
|
+
user_id: 使用者 ID
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
str: 上傳後的永久公開存取 URL
|
|
393
|
+
"""
|
|
394
|
+
try:
|
|
395
|
+
# 1. 從 URL 下載圖片
|
|
396
|
+
image_data, content_type = await download_image_from_url(image_url)
|
|
397
|
+
|
|
398
|
+
# 2. 生成唯一檔案名稱
|
|
399
|
+
import uuid
|
|
400
|
+
from datetime import datetime
|
|
401
|
+
|
|
402
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
403
|
+
file_name = f"dalle_{timestamp}_{uuid.uuid4().hex[:8]}.png"
|
|
404
|
+
|
|
405
|
+
# 3. 使用內部函數上傳
|
|
406
|
+
from botrun_flow_lang.api.storage_api import _upload_img_file_internal
|
|
407
|
+
|
|
408
|
+
public_url = await _upload_img_file_internal(
|
|
409
|
+
user_id, image_data.getvalue(), file_name, content_type
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return public_url
|
|
413
|
+
|
|
414
|
+
except Exception as e:
|
|
415
|
+
import traceback
|
|
416
|
+
|
|
417
|
+
print(f"Error uploading image: {str(e)}")
|
|
418
|
+
traceback.print_exc()
|
|
419
|
+
raise e
|