sabrina91 commited on
Commit
9f58098
·
verified ·
1 Parent(s): 93c80ea

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +628 -0
app.py ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import pandas as pd
4
+ import time
5
+ import json
6
+ from urllib.parse import urlencode
7
+ import resend
8
+ from datetime import datetime
9
+ import io
10
+ from linebot.v3.messaging import Configuration, MessagingApi
11
+ from linebot.v3.messaging.models import TextMessage
12
+ from linebot import LineBotApi
13
+ from linebot.models import TextSendMessage
14
+
15
+ class MeetTaiwanAPIScraper:
16
+ def __init__(self):
17
+ self.base_url = "https://service.meettaiwan.com"
18
+ self.api_base = "https://service.meettaiwan.com/gpa/api/v2/events"
19
+
20
+ # 設定session
21
+ self.session = requests.Session()
22
+ self.session.headers.update({
23
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
24
+ 'Accept': 'application/json, text/plain, */*',
25
+ 'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8',
26
+ 'Accept-Encoding': 'gzip, deflate, br',
27
+ 'Connection': 'keep-alive',
28
+ 'Referer': 'https://service.meettaiwan.com/gpa/zh/events/list;type=all',
29
+ 'Origin': 'https://service.meettaiwan.com'
30
+ })
31
+
32
+ def get_events_by_page(self, page=1, page_size=10, event_type=None, progress=None):
33
+ """調用API獲取指定頁面的活動資料"""
34
+ try:
35
+ # 構建API URL
36
+ params = {
37
+ 'page': page,
38
+ 'pageSize': page_size
39
+ }
40
+
41
+ if event_type:
42
+ params['type'] = event_type
43
+
44
+ # 嘗試不同的API端點
45
+ api_endpoints = [
46
+ f"{self.api_base}/", # 主要API
47
+ f"{self.api_base}/tt-events" # TT events API
48
+ ]
49
+
50
+ for api_url in api_endpoints:
51
+ try:
52
+ response = self.session.get(api_url, params=params, timeout=30)
53
+
54
+ if response.status_code == 200:
55
+ try:
56
+ data = response.json()
57
+
58
+ # 檢查資料結構
59
+ if isinstance(data, dict):
60
+ # 常見的資料結構
61
+ if 'data' in data:
62
+ events = data['data']
63
+ elif 'items' in data:
64
+ events = data['items']
65
+ elif 'results' in data:
66
+ events = data['results']
67
+ elif 'events' in data:
68
+ events = data['events']
69
+ else:
70
+ events = data
71
+
72
+ if isinstance(events, list) and events:
73
+ return events, data
74
+ else:
75
+ continue
76
+
77
+ elif isinstance(data, list):
78
+ return data, data
79
+
80
+ except json.JSONDecodeError as e:
81
+ continue
82
+
83
+ except Exception as e:
84
+ continue
85
+
86
+ return None, None
87
+
88
+ except Exception as e:
89
+ return None, None
90
+
91
+ def get_all_events(self, progress_callback=None):
92
+ """獲取所有類型的活動資料"""
93
+ all_events = []
94
+ page_size_options = [50, 30, 20]
95
+ max_pages = 20
96
+
97
+ for page_size in page_size_options:
98
+ for page in range(1, max_pages + 1):
99
+ if progress_callback:
100
+ progress_callback((page-1) * 5, f"正在獲取第 {page} 頁資料 (頁面大小: {page_size})")
101
+
102
+ events, raw_data = self.get_events_by_page(page=page, page_size=page_size, event_type=None)
103
+
104
+ if not events or len(events) == 0:
105
+ if page == 1:
106
+ break
107
+ else:
108
+ return all_events
109
+
110
+ # 處理事件資料
111
+ for event in events:
112
+ processed_event = self.process_event_data(event, page, 'All')
113
+ if processed_event:
114
+ # 檢查是否已存在(避免重複)
115
+ is_duplicate = False
116
+ for existing_event in all_events:
117
+ if (existing_event['name'] == processed_event['name'] and
118
+ existing_event['event_date'] == processed_event['event_date']):
119
+ is_duplicate = True
120
+ break
121
+
122
+ if not is_duplicate:
123
+ all_events.append(processed_event)
124
+
125
+ # 如果這一頁的資料少於頁面大小,可能是最後一頁
126
+ if len(events) < page_size:
127
+ return all_events
128
+
129
+ time.sleep(0.5) # 減少延遲
130
+
131
+ # 如果成功獲取到資料,就不需要嘗試其他頁面大小
132
+ if all_events:
133
+ break
134
+
135
+ return all_events
136
+
137
+ def process_event_data(self, event, page_num, event_type):
138
+ """處理單個活動資料"""
139
+ try:
140
+ if isinstance(event, dict):
141
+ name = event.get('name', event.get('title', event.get('eventName', '')))
142
+ form = event.get('type', event.get('category', event.get('eventType', event_type or '')))
143
+ event_date = event.get('eventDate', event.get('startDate', event.get('date', '')))
144
+ upload_date = event.get('createdAt', event.get('uploadDate', event.get('publishDate', '')))
145
+
146
+ # 構建連結
147
+ event_id = event.get('id', event.get('eventId', ''))
148
+ if event_id:
149
+ link = f"{self.base_url}/gpa/zh/events/{form}/{event_id}"
150
+ else:
151
+ link = ""
152
+
153
+ return {
154
+ 'name': str(name),
155
+ 'link': link,
156
+ 'form': str(form),
157
+ 'event_date': str(event_date),
158
+ 'upload_date': str(upload_date),
159
+ 'page_num': page_num
160
+ }
161
+
162
+ except Exception as e:
163
+ pass
164
+
165
+ return None
166
+
167
+ def create_html_table(df, max_display=10):
168
+ """將活動資料轉換為 HTML 表格格式"""
169
+ if df is None or df.empty:
170
+ return "<p>沒有找到活動資料</p>"
171
+
172
+ display_count = min(len(df), max_display)
173
+ df_display = df.head(display_count)
174
+
175
+ # 創建 HTML 表格
176
+ html_content = f"""
177
+ <html>
178
+ <head>
179
+ <meta charset="UTF-8">
180
+ <style>
181
+ body {{ font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }}
182
+ .greeting {{
183
+ font-size: 18px;
184
+ color: #2c3e50;
185
+ margin-bottom: 20px;
186
+ font-weight: bold;
187
+ }}
188
+ h2 {{ color: #2c3e50; margin-top: 20px; }}
189
+ .data-source {{
190
+ background-color: #e8f4f8;
191
+ padding: 15px;
192
+ border-left: 4px solid #3498db;
193
+ margin: 20px 0;
194
+ font-weight: bold;
195
+ color: #2c3e50;
196
+ }}
197
+ table {{
198
+ border-collapse: collapse;
199
+ width: 100%;
200
+ margin-top: 20px;
201
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
202
+ }}
203
+ th, td {{
204
+ border: 1px solid #ddd;
205
+ padding: 12px;
206
+ text-align: left;
207
+ }}
208
+ th {{
209
+ background-color: #3498db;
210
+ color: white;
211
+ font-weight: bold;
212
+ }}
213
+ tr:nth-child(even) {{ background-color: #f2f2f2; }}
214
+ tr:hover {{ background-color: #e8f4f8; }}
215
+ a {{ color: #3498db; text-decoration: none; }}
216
+ a:hover {{ text-decoration: underline; }}
217
+ .summary {{
218
+ background-color: #ecf0f1;
219
+ padding: 15px;
220
+ border-radius: 5px;
221
+ margin-bottom: 20px;
222
+ }}
223
+ .copyright {{
224
+ text-align: center;
225
+ margin-top: 30px;
226
+ padding: 20px;
227
+ background-color: #34495e;
228
+ color: white;
229
+ border-radius: 5px;
230
+ font-size: 14px;
231
+ }}
232
+ </style>
233
+ </head>
234
+ <body>
235
+ <div class="greeting">
236
+ 親愛的會員您好:
237
+ </div>
238
+
239
+ <div class="data-source">
240
+ 📋 資料來源:全球政府採購商機網
241
+ </div>
242
+
243
+ <h2>🎯 最新活動資訊</h2>
244
+ <div class="summary">
245
+ <strong>📊 資料統計:</strong>顯示前 {display_count} 筆,共 {len(df)} 筆活動
246
+ </div>
247
+ <table>
248
+ <thead>
249
+ <tr>
250
+ <th>序號</th>
251
+ <th>名稱</th>
252
+ <th>形式</th>
253
+ <th>活動日期</th>
254
+ <th>上載日期</th>
255
+ <th>網址</th>
256
+ </tr>
257
+ </thead>
258
+ <tbody>
259
+ """
260
+
261
+ for idx, row in df_display.iterrows():
262
+ link_html = f'<a href="{row["超連結網址"]}" target="_blank">查看詳情</a>' if row["超連結網址"] else "無連結"
263
+ html_content += f"""
264
+ <tr>
265
+ <td>{idx + 1}</td>
266
+ <td><strong>{row['名稱']}</strong></td>
267
+ <td>{row['形式']}</td>
268
+ <td>{row['活動日期']}</td>
269
+ <td>{row['上載日期']}</td>
270
+ <td>{link_html}</td>
271
+ </tr>
272
+ """
273
+
274
+ html_content += """
275
+ </tbody>
276
+ </table>
277
+ """
278
+
279
+ if len(df) > max_display:
280
+ html_content += f"""
281
+ <div class="summary" style="margin-top: 20px;">
282
+ <strong>📝 提醒:</strong>還有 {len(df) - max_display} 筆資料未顯示,
283
+ 請查看附加的 CSV 檔案獲取完整資料。
284
+ </div>
285
+ """
286
+
287
+ html_content += """
288
+ <div class="summary" style="margin-top: 20px;">
289
+ <strong>🤖 自動爬蟲系統</strong><br>
290
+ 此郵件由 MeetTaiwan API 爬蟲系統自動產生並發送
291
+ </div>
292
+
293
+ <div class="copyright">
294
+ 2025 © Copyright robert_studio
295
+ </div>
296
+ </body>
297
+ </html>
298
+ """
299
+
300
+ return html_content
301
+
302
+ def send_events_email(df_events, recipient_email, api_key, max_display=5):
303
+ """發送活動資料到指定郵箱"""
304
+ if df_events is None or df_events.empty:
305
+ return False, "沒有資料可發送"
306
+
307
+ try:
308
+ # 設定 Resend API Key
309
+ resend.api_key = api_key
310
+
311
+ # 取前N筆資料用於顯示
312
+ df_display = df_events.head(max_display)
313
+
314
+ # 建立 HTML 內容
315
+ html_content = create_html_table(df_display, max_display=max_display)
316
+
317
+ # 準備郵件主題
318
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
319
+ subject = f"📊 MeetTaiwan 最新活動資訊 - {len(df_events)}筆活動 ({current_time})"
320
+
321
+ # 發送郵件
322
+ r = resend.Emails.send({
323
+ "from": "onboarding@resend.dev",
324
+ "to": recipient_email,
325
+ "subject": subject,
326
+ "html": html_content
327
+ })
328
+
329
+ return True, f"郵件發送成功!郵件 ID: {r.get('id', 'N/A')}"
330
+
331
+ except Exception as e:
332
+ return False, f"郵件發送失敗: {str(e)}"
333
+
334
+ def send_line_message(message, channel_access_token):
335
+ """
336
+ Send Line broadcast message to all users who have added the official account
337
+ """
338
+ try:
339
+ # Initialize LineBotApi
340
+ line_bot_api = LineBotApi(channel_access_token)
341
+
342
+ # Create text message
343
+ text_message = TextSendMessage(text=message)
344
+
345
+ # Broadcast message
346
+ line_bot_api.broadcast(text_message)
347
+
348
+ return True, "訊息成功發送到所有用戶!"
349
+
350
+ except Exception as e:
351
+ return False, f"發送訊息時發生錯誤: {e}"
352
+
353
+ def scrape_events(progress=gr.Progress()):
354
+ """爬取活動資料的主函數"""
355
+ try:
356
+ progress(0, desc="初始化爬蟲...")
357
+ scraper = MeetTaiwanAPIScraper()
358
+
359
+ def progress_callback(percent, message):
360
+ progress(percent/100, desc=message)
361
+
362
+ progress(20, desc="開始爬取活動資料...")
363
+ events_data = scraper.get_all_events(progress_callback=progress_callback)
364
+
365
+ if events_data and len(events_data) > 0:
366
+ progress(80, desc="處理資料...")
367
+ # 轉換為 DataFrame
368
+ df_events = pd.DataFrame(events_data)
369
+ df_events.columns = ["名稱", "超連結網址", "形式", "活動日期", "上載日期", "頁數"]
370
+
371
+ # 去除重複資料
372
+ original_count = len(df_events)
373
+ df_events = df_events.drop_duplicates(subset=['名稱', '活動日期'])
374
+ deduplicated_count = len(df_events)
375
+
376
+ progress(100, desc="完成!")
377
+
378
+ success_msg = f"✅ 成功獲取 {deduplicated_count} 筆活動資料!"
379
+ if original_count != deduplicated_count:
380
+ success_msg += f"\n📝 去除了 {original_count - deduplicated_count} 筆重複資料"
381
+
382
+ # 創建下載用的CSV
383
+ csv_data = df_events.to_csv(index=False, encoding='utf-8-sig')
384
+
385
+ return (
386
+ success_msg,
387
+ df_events,
388
+ csv_data,
389
+ f"meettaiwan_events_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
390
+ )
391
+ else:
392
+ return "❌ 無法獲取活動資料,可能原因:API端點變更、需要認證或網路問題", None, None, None
393
+
394
+ except Exception as e:
395
+ return f"❌ 爬取過程發生錯誤: {str(e)}", None, None, None
396
+
397
+ def send_email_report(df_events, recipient_email, api_key, max_display):
398
+ """發送郵件報告"""
399
+ if df_events is None or df_events.empty:
400
+ return "❌ 沒有資料可發送,請先爬取資料"
401
+
402
+ if not api_key.strip():
403
+ return "❌ 請輸入 Resend API Key"
404
+
405
+ if not recipient_email.strip():
406
+ return "❌ 請輸入收件人郵箱"
407
+
408
+ success, message = send_events_email(df_events, recipient_email, api_key, max_display)
409
+
410
+ if success:
411
+ return f"✅ {message}"
412
+ else:
413
+ return f"❌ {message}"
414
+
415
+ def send_line_notification(df_events, channel_access_token, message_template):
416
+ """發送 LINE 通知"""
417
+ if df_events is None or df_events.empty:
418
+ return "❌ 沒有資料可發送,請先爬取資料"
419
+
420
+ if not channel_access_token.strip():
421
+ return "❌ 請輸入 LINE Channel Access Token"
422
+
423
+ try:
424
+ # 準備訊息內容
425
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
426
+
427
+ if message_template.strip():
428
+ message = message_template
429
+ else:
430
+ # 默認訊息模板
431
+ top_5_events = df_events.head(5)
432
+ events_list = ""
433
+ for idx, row in top_5_events.iterrows():
434
+ events_list += f"{idx+1}. {row['名稱']} ({row['活動日期']})\n"
435
+
436
+ message = f"""🎯 MeetTaiwan 最新活動通知
437
+
438
+ 📊 共找到 {len(df_events)} 筆活動資料
439
+ ⏰ 更新時間:{current_time}
440
+
441
+ 📋 最新 5 筆活動:
442
+ {events_list}
443
+
444
+ 🤖 此訊息由自動爬蟲系統發送
445
+ 資料來源:全球政府採購商機網"""
446
+
447
+ success, result_message = send_line_message(message, channel_access_token)
448
+
449
+ if success:
450
+ return f"✅ {result_message}"
451
+ else:
452
+ return f"❌ {result_message}"
453
+
454
+ except Exception as e:
455
+ return f"❌ 發送 LINE 通知時發生錯誤: {str(e)}"
456
+
457
+ # 創建 Gradio 界面
458
+ def create_interface():
459
+ with gr.Blocks(
460
+ title="🎯 MeetTaiwan 活動爬蟲系統",
461
+ theme=gr.themes.Soft(),
462
+ css="""
463
+ .gradio-container {
464
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
465
+ }
466
+ .gr-button-primary {
467
+ background: linear-gradient(45deg, #3498db, #2ecc71) !important;
468
+ border: none !important;
469
+ }
470
+ .gr-button-secondary {
471
+ background: linear-gradient(45deg, #e74c3c, #f39c12) !important;
472
+ border: none !important;
473
+ }
474
+ """
475
+ ) as app:
476
+
477
+ # 標題區域
478
+ gr.Markdown("""
479
+ # 🎯 MeetTaiwan 活動爬蟲系統
480
+ **全球政府採購商機網活動資訊自動抓取與通知系統**
481
+
482
+ 本系統整合了資料爬取、郵件發送和 LINE 通知功能,讓您輕鬆獲取最新的活動資訊。
483
+ """)
484
+
485
+ # 狀態變量
486
+ scraped_data = gr.State(None)
487
+
488
+ with gr.Tabs():
489
+ # Tab 1: 資料爬取
490
+ with gr.Tab("📊 資料爬取", id="scraping"):
491
+ with gr.Row():
492
+ with gr.Column(scale=2):
493
+ gr.Markdown("### 🚀 開始爬取活動資料")
494
+ scrape_btn = gr.Button(
495
+ "開始爬取",
496
+ variant="primary",
497
+ size="lg"
498
+ )
499
+
500
+ scrape_status = gr.Textbox(
501
+ label="爬取狀態",
502
+ interactive=False,
503
+ placeholder="等待開始爬取..."
504
+ )
505
+
506
+ with gr.Column(scale=1):
507
+ gr.Markdown("### 📥 資料下載")
508
+ download_file = gr.File(
509
+ label="下載 CSV 檔案",
510
+ interactive=False
511
+ )
512
+
513
+ # 資料預覽
514
+ gr.Markdown("### 📋 資料預覽")
515
+ data_preview = gr.Dataframe(
516
+ label="活動資料",
517
+ wrap=True,
518
+ interactive=False
519
+ )
520
+
521
+ # Tab 2: 郵件發送
522
+ with gr.Tab("📧 郵件通知", id="email"):
523
+ with gr.Row():
524
+ with gr.Column():
525
+ gr.Markdown("### ⚙️ 郵件設定")
526
+ email_api_key = gr.Textbox(
527
+ label="Resend API Key",
528
+ type="password",
529
+ value="re_ZGacBiDw_HFEBpuCbaJ2S3NThPWiMU7Ex",
530
+ placeholder="請輸入您的 Resend API Key"
531
+ )
532
+ recipient_email = gr.Textbox(
533
+ label="收件人郵箱",
534
+ value="cjhuang38@gmail.com",
535
+ placeholder="請輸入收件人郵箱地址"
536
+ )
537
+ max_display_email = gr.Slider(
538
+ label="郵件顯示筆數",
539
+ minimum=5,
540
+ maximum=20,
541
+ value=10,
542
+ step=1
543
+ )
544
+
545
+ with gr.Column():
546
+ gr.Markdown("### 📨 發送郵件")
547
+ send_email_btn = gr.Button(
548
+ "發送郵件報告",
549
+ variant="secondary",
550
+ size="lg"
551
+ )
552
+ email_status = gr.Textbox(
553
+ label="郵件發送狀態",
554
+ interactive=False,
555
+ placeholder="等待發送..."
556
+ )
557
+
558
+ # Tab 3: LINE 通知
559
+ with gr.Tab("📱 LINE 通知", id="line"):
560
+ with gr.Row():
561
+ with gr.Column():
562
+ gr.Markdown("### ⚙️ LINE 設定")
563
+ line_token = gr.Textbox(
564
+ label="LINE Channel Access Token",
565
+ type="password",
566
+ value="FuM3pGmpqyOldcMltKVxkzBuy32o6mpkWv/gVfrR3sm9VxFUxTVzLlKU9C1ssOi2l/om2JkpKIdB/R+VLAyCvQA2o4pTD757kpN4GmUUq68FKuWwEaQXG376pR8hhyqUvElGn4rEYA7oxJDgsm4EBAdB04t89/1O/w1cDnyilFU=",
567
+ placeholder="請輸入您的 LINE Channel Access Token"
568
+ )
569
+ message_template = gr.Textbox(
570
+ label="自訂訊息內容 (選填)",
571
+ placeholder="留空將使用默認模板...",
572
+ lines=5
573
+ )
574
+
575
+ with gr.Column():
576
+ gr.Markdown("### 📲 發送通知")
577
+ send_line_btn = gr.Button(
578
+ "發送 LINE 通知",
579
+ variant="secondary",
580
+ size="lg"
581
+ )
582
+ line_status = gr.Textbox(
583
+ label="LINE 發送狀態",
584
+ interactive=False,
585
+ placeholder="等待發送..."
586
+ )
587
+
588
+ # 頁尾
589
+ gr.Markdown("""
590
+ ---
591
+ <div style='text-align: center; color: #666; margin-top: 20px;'>
592
+ <p>🤖 MeetTaiwan API 爬蟲系統 | 2025 © Copyright robert_studio</p>
593
+ <p>資料來源:全球政府採購商機網</p>
594
+ </div>
595
+ """)
596
+
597
+ # 事件綁定
598
+ scrape_btn.click(
599
+ fn=scrape_events,
600
+ outputs=[scrape_status, scraped_data, download_file, gr.State()]
601
+ ).then(
602
+ fn=lambda data: data if data is not None else gr.DataFrame(),
603
+ inputs=[scraped_data],
604
+ outputs=[data_preview]
605
+ )
606
+
607
+ send_email_btn.click(
608
+ fn=send_email_report,
609
+ inputs=[scraped_data, recipient_email, email_api_key, max_display_email],
610
+ outputs=[email_status]
611
+ )
612
+
613
+ send_line_btn.click(
614
+ fn=send_line_notification,
615
+ inputs=[scraped_data, line_token, message_template],
616
+ outputs=[line_status]
617
+ )
618
+
619
+ return app
620
+
621
+ if __name__ == "__main__":
622
+ app = create_interface()
623
+ app.launch(
624
+ server_name="0.0.0.0",
625
+ server_port=7860,
626
+ share=True,
627
+ show_error=True
628
+ )