botrun-flow-lang 5.10.82__py3-none-any.whl → 5.10.83__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.
Files changed (84) hide show
  1. botrun_flow_lang/api/auth_api.py +39 -39
  2. botrun_flow_lang/api/auth_utils.py +183 -183
  3. botrun_flow_lang/api/botrun_back_api.py +65 -65
  4. botrun_flow_lang/api/flow_api.py +3 -3
  5. botrun_flow_lang/api/hatch_api.py +481 -481
  6. botrun_flow_lang/api/langgraph_api.py +796 -796
  7. botrun_flow_lang/api/line_bot_api.py +1357 -1357
  8. botrun_flow_lang/api/model_api.py +300 -300
  9. botrun_flow_lang/api/rate_limit_api.py +32 -32
  10. botrun_flow_lang/api/routes.py +79 -79
  11. botrun_flow_lang/api/search_api.py +53 -53
  12. botrun_flow_lang/api/storage_api.py +316 -316
  13. botrun_flow_lang/api/subsidy_api.py +290 -290
  14. botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
  15. botrun_flow_lang/api/user_setting_api.py +70 -70
  16. botrun_flow_lang/api/version_api.py +31 -31
  17. botrun_flow_lang/api/youtube_api.py +26 -26
  18. botrun_flow_lang/constants.py +13 -13
  19. botrun_flow_lang/langgraph_agents/agents/agent_runner.py +174 -174
  20. botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
  21. botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
  22. botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
  23. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  25. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +591 -548
  26. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  27. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  28. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  29. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  30. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  31. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
  32. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +345 -345
  33. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  34. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  35. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +160 -160
  36. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  37. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  38. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  39. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  40. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  41. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  42. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  43. botrun_flow_lang/log/.gitignore +2 -2
  44. botrun_flow_lang/main.py +61 -61
  45. botrun_flow_lang/main_fast.py +51 -51
  46. botrun_flow_lang/mcp_server/__init__.py +10 -10
  47. botrun_flow_lang/mcp_server/default_mcp.py +711 -711
  48. botrun_flow_lang/models/nodes/utils.py +205 -205
  49. botrun_flow_lang/models/token_usage.py +34 -34
  50. botrun_flow_lang/requirements.txt +21 -21
  51. botrun_flow_lang/services/base/firestore_base.py +30 -30
  52. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  53. botrun_flow_lang/services/hatch/hatch_fs_store.py +372 -372
  54. botrun_flow_lang/services/storage/storage_cs_store.py +202 -202
  55. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  56. botrun_flow_lang/services/storage/storage_store.py +65 -65
  57. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  58. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  59. botrun_flow_lang/static/docs/tools/index.html +926 -926
  60. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  61. botrun_flow_lang/tests/api_stress_test.py +357 -357
  62. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  63. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  64. botrun_flow_lang/tests/test_html_util.py +31 -31
  65. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  66. botrun_flow_lang/tests/test_img_util.py +39 -39
  67. botrun_flow_lang/tests/test_local_files.py +114 -114
  68. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  69. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  70. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  71. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  72. botrun_flow_lang/tools/generate_docs.py +133 -133
  73. botrun_flow_lang/tools/templates/tools.html +153 -153
  74. botrun_flow_lang/utils/__init__.py +7 -7
  75. botrun_flow_lang/utils/botrun_logger.py +344 -344
  76. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  77. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  78. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  79. botrun_flow_lang/utils/langchain_utils.py +324 -324
  80. botrun_flow_lang/utils/yaml_utils.py +9 -9
  81. {botrun_flow_lang-5.10.82.dist-info → botrun_flow_lang-5.10.83.dist-info}/METADATA +3 -2
  82. botrun_flow_lang-5.10.83.dist-info/RECORD +99 -0
  83. botrun_flow_lang-5.10.82.dist-info/RECORD +0 -99
  84. {botrun_flow_lang-5.10.82.dist-info → botrun_flow_lang-5.10.83.dist-info}/WHEEL +0 -0
@@ -1,345 +1,345 @@
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_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_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_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
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_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_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_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