import gradio as gr
import datetime
import pandas as pd
import warnings
import joblib
import hdbscan
import numpy as np
import logger_service
import os
import urllib.request
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.font_manager as fm
import shutil
import io
import base64
# --- 🌟 解決 Matplotlib 中文顯示問題 ---
font_path = 'SimHei.ttf'
if not os.path.exists(font_path):
try:
print("📥 正在下載中文字型...")
urllib.request.urlretrieve('https://github.com/StellarCN/scp_zh/raw/master/fonts/SimHei.ttf', font_path)
except Exception as e:
print(f"⚠️ 字型下載失敗: {e}")
if os.path.exists(font_path):
fm.fontManager.addfont(font_path)
plt.rc('font', family='SimHei')
plt.rcParams['axes.unicode_minus'] = False
# Seaborn 近期會對舊的 pandas option 發出 FutureWarning(純警告、非功能錯誤)
# 重整前端會觸發繪圖端點,因此你會在 terminal 看到重複訊息。
warnings.filterwarnings(
"ignore",
category=FutureWarning,
message="use_inf_as_na option is deprecated*",
)
# --- 全域統計與狀態變數 ---
global_stats = {
"total_count": 0,
"abnormal_count": 0,
"start_time": datetime.datetime.now()
}
system_weights = {
"ow": 75.0, "tw": 80.0, "iw": 65.0,
"dw": 45.0, "gw": 50.0, "fw": 60.0
}
def update_system_weights(ow, tw, iw, dw, gw, fw):
system_weights.update({
"ow": float(ow), "tw": float(tw), "iw": float(iw),
"dw": float(dw), "gw": float(gw), "fw": float(fw)
})
return f"✅ 系統權重已更新 - 總體: {ow}%, 時間: {tw}%, IP: {iw}%, 裝置: {dw}%, 地理: {gw}%, 頻率: {fw}%"
ACTION_MAP = {
"教務系統: 期末網路教學評量": "click_eval_system",
"教務系統: 期末網路預選系統": "click_pre_select_system",
"教務系統: 開學後加退選系統": "click_add_drop_system",
"教務系統: 北科i學園PLUS": "click_istudy",
"教務系統: 學生證掛失及補發系統": "click_card_loss",
"教務系統: 課程系統": "click_course_system",
"教務系統: 學業成績查詢系統": "click_score_query",
"學務系統: 學生請假系統": "click_leave_system",
"學務系統: 學生宿舍登錄(抽籤)系統": "click_dorm_system",
"學務系統: 獎助學金申請系統": "click_scholarship",
"資訊服務: 網路郵局 WebMail": "click_webmail",
"資訊服務: 北科軟體雲": "click_vdesk",
"惡意測試: 嘗試偷改成績 (SQL Injection)": "malicious_sql_injection"
}
log_history = []
initial_df_abn = pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"])
# HDBSCAN 特徵分群用的 IP 分類
def get_ip_category(ip_str):
ip = str(ip_str).split(" ")[0]
if ip.startswith("140.124."): return 0.0
elif ip.startswith("61.228."): return 1.0
elif ip.startswith("210.61."): return 2.0
elif ip.startswith("45.33."): return 3.0
else: return 4.0
def get_geo_level(ip_str):
ip = str(ip_str).split(" ")[0]
if ip == "140.124.71.55": return 0
elif ip == "140.124.18.22": return 1
elif ip.startswith("61.228."): return 2
elif ip.startswith("210.61."): return 3
elif ip.startswith("45.33."): return 4
else: return 5
# --- 1. 載入模型與歷史資料 ---
try:
le_action = joblib.load('le_action.pkl')
print("📂 正在讀取本地歷史資料庫...")
if os.path.exists('baseline_logs.csv'):
baseline_df = pd.read_csv('baseline_logs.csv')
else:
baseline_df = pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"])
baseline_df.to_csv('baseline_logs.csv', index=False)
baseline_df['timestamp'] = pd.to_datetime(baseline_df['timestamp'])
baseline_df['clean_ip'] = baseline_df['ip_address'].astype(str).apply(lambda x: x.split(" ")[0])
global_stats["total_count"] = len(baseline_df)
global_stats["abnormal_count"] = baseline_df[baseline_df['status'].str.contains("異常", na=False)].shape[0]
print(f"📊 統計初始化:總監測量 {global_stats['total_count']}, 異常數 {global_stats['abnormal_count']}")
display_df = baseline_df.sort_values(by='timestamp', ascending=False).head(20).copy()
display_df['timestamp'] = display_df['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
log_history = display_df[['timestamp', 'ip_address', 'cookie_id', 'account', 'role', 'action', 'status']].to_dict('records')
abnormal_mask = baseline_df['status'].astype(str).str.contains("異常", na=False)
if abnormal_mask.any():
abn_df_temp = baseline_df[abnormal_mask].sort_values(by='timestamp', ascending=False).copy()
abn_df_temp['timestamp'] = abn_df_temp['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
initial_df_abn = abn_df_temp[['timestamp', 'ip_address', 'cookie_id', 'account', 'role', 'action', 'status']]
baseline_df['month'] = baseline_df['timestamp'].dt.month
baseline_df['day'] = baseline_df['timestamp'].dt.day
baseline_df['hour'] = baseline_df['timestamp'].dt.hour
ip_codes = np.array([get_ip_category(ip) for ip in baseline_df['clean_ip'].values])
actions = baseline_df['action'].astype(str).values
known_actions = set(le_action.classes_)
unknown_actions = set(actions) - known_actions
if unknown_actions: le_action.classes_ = np.append(le_action.classes_, list(unknown_actions))
baseline_X = np.column_stack((
baseline_df['month'].values, baseline_df['day'].values, baseline_df['hour'].values,
ip_codes, le_action.transform(actions)
))
AI_READY = True
print("✅ AI 模組與歷史基準資料載入成功!")
except Exception as e:
AI_READY = False
global_stats["total_count"] = 0; global_stats["abnormal_count"] = 0
print(f"⚠️ AI 模組載入失敗,請確認檔案。錯誤: {e}")
USER_DB = {"student": ["1234", "學生", "同學"], "admin": ["1234", "管理", "管理員"]}
# =====================================================================
# 👉 新增:純淨 API 端點專用函式 (只回傳 JSON 字典,給隊友抓資料用)
# =====================================================================
def api_get_stats():
"""取得系統狀態與權重"""
total = global_stats["total_count"]
abnormal = global_stats["abnormal_count"]
rate = round((abnormal / total * 100), 2) if total > 0 else 0
return {
"status": "success",
"ai_ready": AI_READY,
"data": {
"total_logs": total,
"abnormal_logs": abnormal,
"anomaly_rate_percent": rate,
"current_weights": system_weights
}
}
def api_get_logs(limit=20):
"""取得最新的 N 筆 Log"""
try: limit = int(limit)
except: limit = 20
return {
"status": "success",
"count": len(log_history[:limit]),
"data": log_history[:limit]
}
def api_get_chart_data(action_name):
"""取得特定系統的圖表原始數據 (供前端自行繪圖)"""
if not AI_READY: return {"status": "error", "message": "AI 模型未載入"}
action_code = ACTION_MAP.get(action_name)
if not action_code: return {"status": "error", "message": f"找不到系統: {action_name}"}
df_hist = baseline_df[baseline_df['action'] == action_code].copy()
df_live = pd.DataFrame(log_history)
if not df_live.empty:
df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce')
df_live = df_live.dropna(subset=['timestamp'])
df_live = df_live[df_live['action'] == action_code].copy()
if not df_live.empty:
df_combined = pd.concat([df_hist, df_live], ignore_index=True)
df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action'])
else:
df_combined = df_hist.copy()
if df_combined.empty:
return {"status": "success", "action": action_code, "data": {"normal": [], "abnormal": []}}
df_combined['month'] = df_combined['timestamp'].dt.month
df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0
is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False)
normal_data = df_combined[~is_abnormal][['month', 'time_of_day', 'timestamp']].astype(str).to_dict('records')
abnormal_data = df_combined[is_abnormal][['month', 'time_of_day', 'timestamp', 'status']].astype(str).to_dict('records')
return {
"status": "success",
"action_code": action_code,
"data": {"normal": normal_data, "abnormal": abnormal_data}
}
def api_get_abnormal_logs():
"""取得所有異常 Log"""
abnormal_logs = [log for log in log_history if "異常" in str(log.get("status", "")) or "錯誤" in str(log.get("status", "")) or "受限" in str(log.get("status", ""))]
return {
"status": "success",
"count": len(abnormal_logs),
"data": abnormal_logs
}
# =====================================================================
def get_dashboard_html():
total = global_stats["total_count"]
abnormal = global_stats["abnormal_count"]
rate = (abnormal / total * 100) if total > 0 else 0
if not AI_READY: status_icon, status_text, status_color, status_desc = "🔴", "系統錯誤", "#ef4444", "AI 模型載入失敗,防護停用中。"
elif rate > 10: status_icon, status_text, status_color, status_desc = "🟡", "高風險警告", "#f59e0b", "近期異常行為頻發,請立刻檢查 Log。"
else: status_icon, status_text, status_color, status_desc = "🟢", "正常執行", "#10b981", "HDBSCAN 與 Agent 服務運作正常。"
return f"""
📊
總監測量 (本地歷史資料庫)
{total:,} 筆 Log
自系統啟動起統計 (+今日即時)
⚠️
異常率 (平均數值)
共 {abnormal:,} 筆監測到異常行為
{status_icon}
系統狀態
{status_text}
{status_desc}
"""
def get_plot_data_for_api(selected_key):
"""API專用版本,返回Base64圖片字符串"""
if not selected_key or not AI_READY: return None
action_code = ACTION_MAP.get(selected_key)
try:
plt.close('all')
df_hist = baseline_df[baseline_df['action'] == action_code].copy()
df_live = pd.DataFrame(log_history)
if not df_live.empty:
df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce')
df_live = df_live.dropna(subset=['timestamp'])
df_live = df_live[df_live['action'] == action_code].copy()
if not df_live.empty:
df_combined = pd.concat([df_hist, df_live], ignore_index=True)
df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action'])
else:
df_combined = df_hist.copy()
if df_combined.empty:
fig, ax = plt.subplots(figsize=(12, 6))
if os.path.exists(font_path): plt.rc('font', family='SimHei')
ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
# 將圖片轉換為 Base64
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
buf.close()
plt.close(fig)
return f"data:image/png;base64,{img_base64}"
df_combined['month'] = df_combined['timestamp'].dt.month
df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0
is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False)
df_normal = df_combined[~is_abnormal]
df_abnormal = df_combined[is_abnormal]
fig, ax = plt.subplots(figsize=(12, 6))
sns.set_theme(style="whitegrid")
if os.path.exists(font_path): plt.rc('font', family='SimHei')
plt.rcParams['axes.unicode_minus'] = False
has_data = False
months_order = list(range(1, 13))
if not df_normal.empty:
has_data = True
sns.stripplot(data=df_normal, x='month', y='time_of_day', jitter=0.3, alpha=0.4, size=5, color='#10b981', order=months_order, ax=ax, label='正常資料')
if not df_abnormal.empty:
has_data = True
sns.stripplot(data=df_abnormal, x='month', y='time_of_day', jitter=0.3, alpha=0.9, size=9, color='#ef4444', marker='X', order=months_order, ax=ax, label='異常資料')
if not has_data:
ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
# 將圖片轉換為 Base64
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
buf.close()
plt.close(fig)
return f"data:image/png;base64,{img_base64}"
ax.set_xlabel('月份 (1 ~ 12月)', fontsize=12)
ax.set_ylabel('時間 (24小時制)', fontsize=12)
ax.set_yticks(np.arange(0, 25, 2))
ax.set_ylim(24.5, -0.5)
handles, labels = ax.get_legend_handles_labels()
by_label = dict(zip(labels, handles))
if by_label: ax.legend(by_label.values(), by_label.keys(), loc='upper right', bbox_to_anchor=(1.15, 1))
plt.tight_layout()
# 將圖片轉換為 Base64
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
buf.close()
plt.close(fig)
return f"data:image/png;base64,{img_base64}"
except Exception as e:
print(f"繪圖失敗: {e}")
# 返回一個簡單的錯誤圖片
fig, ax = plt.subplots(figsize=(8, 4))
ax.text(0.5, 0.5, f"圖表生成失敗: {str(e)}", ha='center', va='center', fontsize=12, color='red')
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis('off')
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
buf.close()
plt.close(fig)
return f"data:image/png;base64,{img_base64}"
def get_plot_data(selected_key):
"""給Gradio界面使用,返回Figure對象"""
if not selected_key or not AI_READY: return None
action_code = ACTION_MAP.get(selected_key)
try:
plt.close('all')
df_hist = baseline_df[baseline_df['action'] == action_code].copy()
df_live = pd.DataFrame(log_history)
if not df_live.empty:
df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce')
df_live = df_live.dropna(subset=['timestamp'])
df_live = df_live[df_live['action'] == action_code].copy()
if not df_live.empty:
df_combined = pd.concat([df_hist, df_live], ignore_index=True)
df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action'])
else:
df_combined = df_hist.copy()
if df_combined.empty:
fig, ax = plt.subplots(figsize=(12, 6))
if os.path.exists(font_path): plt.rc('font', family='SimHei')
ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
return fig
df_combined['month'] = df_combined['timestamp'].dt.month
df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0
is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False)
df_normal = df_combined[~is_abnormal]
df_abnormal = df_combined[is_abnormal]
fig, ax = plt.subplots(figsize=(12, 6))
sns.set_theme(style="whitegrid")
if os.path.exists(font_path): plt.rc('font', family='SimHei')
plt.rcParams['axes.unicode_minus'] = False
has_data = False
months_order = list(range(1, 13))
if not df_normal.empty:
has_data = True
sns.stripplot(data=df_normal, x='month', y='time_of_day', jitter=0.3, alpha=0.4, size=5, color='#10b981', order=months_order, ax=ax, label='正常資料')
if not df_abnormal.empty:
has_data = True
sns.stripplot(data=df_abnormal, x='month', y='time_of_day', jitter=0.3, alpha=0.9, size=9, color='#ef4444', marker='X', order=months_order, ax=ax, label='異常資料')
if not has_data:
ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
return fig
ax.set_xlabel('月份 (1 ~ 12月)', fontsize=12)
ax.set_ylabel('時間 (24小時制)', fontsize=12)
ax.set_yticks(np.arange(0, 25, 2))
ax.set_ylim(24.5, -0.5)
handles, labels = ax.get_legend_handles_labels()
by_label = dict(zip(labels, handles))
if by_label: ax.legend(by_label.values(), by_label.keys(), loc='upper right', bbox_to_anchor=(1.15, 1))
plt.tight_layout()
return fig
except Exception as e:
print(f"繪圖失敗: {e}")
return None
def timer_update(selected_key): return get_dashboard_html(), get_plot_data(selected_key)
def explain_abnormal_log(log_text):
if not log_text or log_text.strip() == "": return "⚠️ 請先提供異常 Log 資訊以進行分析。"
try: from groq import Groq
except ImportError: return "⚠️ 未安裝 groq 套件。請在終端機執行 `pip install groq`。"
groq_api_key = os.environ.get("GROQ_API_KEY", "")
if not groq_api_key: return "⚠️ 未設定 GROQ_API_KEY。"
try:
client = Groq(api_key=groq_api_key)
response = client.chat.completions.create(
messages=[
{"role": "system", "content": "你是一位專業的資安分析師。請簡單解釋為什麼這筆 Log 異常,並指出風險(如撞庫攻擊、作息異常等)。"},
{"role": "user", "content": f"請分析這筆異常 Log:\n{log_text}"}
],
model="llama-3.1-8b-instant", temperature=0.5, max_tokens=300
)
return response.choices[0].message.content
except Exception as e: return f"⚠️ 呼叫 API 發生錯誤: {str(e)}"
# --- 2. 核心邏輯與真實 HDBSCAN 異常偵測 ---
def check_anomaly(ip, account, action, log_time):
if not AI_READY: return "success"
if action in ["login_attempt", "logout"]: return "success"
try:
ow = float(system_weights["ow"])
tw = float(system_weights["tw"])
iw = float(system_weights["iw"])
dw = float(system_weights["dw"])
gw = float(system_weights["gw"])
fw = float(system_weights["fw"])
geo_level = get_geo_level(ip)
if gw == 100 and geo_level > 0:
return "⚠️異常_地理位置受限 (100%絕對鎖定: 僅限校內專網存取)"
elif gw >= 90 and geo_level > 1:
return "⚠️異常_地理位置受限 (高敏感防護: 僅限校園網路與宿舍)"
elif gw >= 70 and geo_level > 2:
return "⚠️異常_地理位置受限 (進階防護: 禁止公共場所與海外連線)"
elif gw >= 50 and geo_level > 3:
return "⚠️異常_地理位置受限 (預設防護: 禁止海外異常 IP)"
current_dt = pd.to_datetime(str(log_time))
cutoff_dt = current_dt - pd.Timedelta(seconds=60)
recent_clicks = 0
for log in log_history:
log_dt = pd.to_datetime(log['timestamp'])
if log_dt >= cutoff_dt:
if log.get('account') == account and log.get('action') == action:
recent_clicks += 1
else:
break
if fw <= 0: max_allowed_clicks = 100
elif fw <= 10: max_allowed_clicks = 90
elif fw <= 20: max_allowed_clicks = 80
elif fw <= 30: max_allowed_clicks = 70
elif fw <= 40: max_allowed_clicks = 60
elif fw <= 50: max_allowed_clicks = 50
elif fw <= 60: max_allowed_clicks = 40
elif fw <= 70: max_allowed_clicks = 30
elif fw <= 80: max_allowed_clicks = 15
elif fw <= 90: max_allowed_clicks = 9
else: max_allowed_clicks = 3
if (recent_clicks + 1) > max_allowed_clicks:
return f"⚠️異常_單一操作頻率過高 ({recent_clicks+1}次/分)"
ip_code = float(get_ip_category(ip))
action_code = float(le_action.transform([action])[0])
current_month = float(current_dt.month)
current_day = float(current_dt.day)
current_hour = float(current_dt.hour)
time_multiplier = tw / 100.0
action_scale = (dw / 100.0) * 100.0
scaled_baseline = baseline_X.copy().astype(float)
scaled_baseline[:, 0] *= time_multiplier * 10.0
scaled_baseline[:, 1] *= time_multiplier * 0.5
scaled_baseline[:, 2] *= time_multiplier * 2.0
scaled_baseline[:, 3] *= (iw / 100.0) * 10.0
scaled_baseline[:, 4] *= action_scale
v_month = current_month * time_multiplier * 10.0
v_day = current_day * time_multiplier * 0.5
v_hour = current_hour * time_multiplier * 2.0
v_ip = ip_code * (iw / 100.0) * 10.0
v_action = action_code * action_scale
new_data_point = np.array([[v_month, v_day, v_hour, v_ip, v_action]], dtype=float)
same_action_mask = (baseline_X[:, 4] == action_code)
relevant_history = scaled_baseline[same_action_mask]
other_history = scaled_baseline[~same_action_mask]
if len(other_history) > 800:
np.random.seed(int(current_day + current_hour))
indices = np.random.choice(len(other_history), 800, replace=False)
other_history = other_history[indices]
fit_X = np.vstack((other_history, relevant_history, new_data_point))
strictness = max(2, int(2 + (ow / 100.0) * 4))
model = hdbscan.HDBSCAN(min_cluster_size=strictness, min_samples=2)
clusters = model.fit_predict(fit_X)
if clusters[-1] == -1: return "⚠️異常_作息或行為不符"
if np.any(same_action_mask):
distances = np.linalg.norm(relevant_history - new_data_point[0], axis=1)
min_distance = np.min(distances)
max_tolerance_distance = time_multiplier * 15.0 + 1.0
if ip_code >= 3.0: max_tolerance_distance *= (1 - (iw / 100.0) * 0.6)
if min_distance > max_tolerance_distance:
return "⚠️異常_作息或行為不符"
return "success"
except ValueError:
return "⚠️異常_未知特徵 (出現未授權的 IP 或操作)"
except Exception as e:
return f"⚠️系統錯誤: {str(e)}"
def record_log(ip, cookie, account, role, action, status, time_mode, custom_time):
global baseline_X
if time_mode == "自訂時間 (模擬過去/未來)" and custom_time and str(custom_time).strip() != "":
try:
valid_time = pd.to_datetime(str(custom_time))
log_time = valid_time.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
log_time = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S")
status = "⚠️格式錯誤_無效的自訂時間"
else:
log_time = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S")
if status == "success":
ai_judgment = check_anomaly(ip, account, action, log_time)
if ai_judgment != "success": status = ai_judgment
new_log = {"timestamp": log_time, "ip_address": ip, "cookie_id": cookie, "account": account, "role": role, "action": action, "status": status}
new_df = pd.DataFrame([new_log])
new_df.to_csv('baseline_logs.csv', mode='a', header=not os.path.exists('baseline_logs.csv'), index=False)
try: logger_service.push_new_log(new_log)
except: pass
global_stats["total_count"] += 1
if "異常" in str(status) or "錯誤" in str(status) or "受限" in str(status): global_stats["abnormal_count"] += 1
log_history.insert(0, new_log)
if status == "success" and AI_READY:
try:
current_time = pd.to_datetime(str(log_time))
if action not in le_action.classes_: le_action.classes_ = np.append(le_action.classes_, action)
new_row = np.array([[float(current_time.month), float(current_time.day), float(current_time.hour), float(get_ip_category(ip)), float(le_action.transform([action])[0])]], dtype=float)
baseline_X = np.vstack((baseline_X, new_row))
except: pass
df_all = pd.DataFrame(log_history)
abnormal_logs = [log for log in log_history if "異常" in str(log.get("status", "")) or "錯誤" in str(log.get("status", "")) or "受限" in str(log.get("status", ""))]
df_abnormal = pd.DataFrame(abnormal_logs) if abnormal_logs else pd.DataFrame(columns=df_all.columns)
input_update = gr.update(value=f"時間: {log_time} | IP: {ip} | 帳號: {account} | 動作: {action} | 狀態: {status}") if "異常" in str(status) or "錯誤" in str(status) or "受限" in str(status) else gr.update()
return df_all, df_abnormal, input_update
def fast_login(target_account, ip, cookie, time_mode, custom_time):
role, name = USER_DB[target_account][1], USER_DB[target_account][2]
df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time)
target_tab = "student_tab" if role == "學生" else "admin_tab"
return gr.update(selected="dashboard_route"), gr.update(selected=target_tab), "", f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)", df_all, df_abn, inp_upd, target_account
def process_login(ip, cookie, account, password, captcha, time_mode, custom_time):
def fail_response(msg, df_all, df_abn, inp_upd): return gr.update(selected="login_route"), gr.update(), msg, gr.update(), df_all, df_abn, inp_upd
if account not in USER_DB:
df_all, df_abn, inp_upd = record_log(ip, cookie, account, "Unknown", "login_attempt", "failed_no_account", time_mode, custom_time)
return fail_response("帳號不存在!", df_all, df_abn, inp_upd)
role, name = USER_DB[account][1], USER_DB[account][2]
if captcha.upper() != "FEIK":
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_captcha", time_mode, custom_time)
return fail_response("驗證碼錯誤!", df_all, df_abn, inp_upd)
if USER_DB[account][0] == password:
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time)
return gr.update(selected="dashboard_route"), gr.update(selected="student_tab" if role == "學生" else "admin_tab"), "", f"**{name}** 歡迎您 | 線上人數: 938", df_all, df_abn, inp_upd
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time)
return fail_response("密碼錯誤!", df_all, df_abn, inp_upd)
def toggle_role(current_account, ip, cookie, time_mode, custom_time):
target_account = "student" if current_account == "admin" else "admin"
role, name = USER_DB[target_account][1], USER_DB[target_account][2]
df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time)
return gr.update(selected="student_tab" if role == "學生" else "admin_tab"), f"**{name}** 歡迎您 | 線上人數: 938 (⚡單鍵切換模式)", df_all, df_abn, inp_upd, target_account
def logout(ip, cookie, account, time_mode, custom_time):
role = USER_DB.get(account, ["", "Unknown"])[1]
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time)
return gr.update(selected="login_route"), "", "", "", "", df_all, df_abn, inp_upd
def simulate_click(ip, cookie, account, button_name, time_mode, custom_time):
role = USER_DB.get(account, ["", "Unknown"])[1]
return record_log(ip, cookie, account, role, button_name, "success", time_mode, custom_time)
def _extract_gr_update_value(update_obj):
"""
Gradio 的 gr.update(...) 可能回傳 dict 形式或物件形式。
這裡統一抓出其中的 `value`,否則回傳 None。
"""
if update_obj is None:
return None
if isinstance(update_obj, dict):
return update_obj.get("value")
# Fallback: try attribute access
try:
return getattr(update_obj, "value", None)
except Exception:
return None
def api_run_student_action(ip, c, acc, button_name, tm, ct):
"""
統一的 student action API handler:
- 呼叫 simulate_click 真正寫入 log
- 只回傳異常/受限/錯誤時 abnormal_log_input 的字串(正常時回傳空字串)
"""
df_all, df_abn, inp_upd = simulate_click(ip, c, acc, button_name, tm, ct)
val = _extract_gr_update_value(inp_upd)
return val or ""
def api_run_student_action_code(ip, c, acc, action_code, tm, ct):
"""
Generic student action API.
Vue 會直接傳 action_code(例如 click_eval_system / malicious_sql_injection)
"""
df_all, df_abn, inp_upd = simulate_click(ip, c, acc, action_code, tm, ct)
val = _extract_gr_update_value(inp_upd)
# 回傳 store 的新資料(讓前端 update_dataframes 能讀到),以及異常字串給 Vue 顯示
return df_all, df_abn, val or ""
def api_lambda(ip, c, tm, ct):
# Vue: lambda(期末教學評量) 沒有傳 acc => 預設用學生帳號
default_acc = USER_DB["student"][0]
return api_run_student_action(ip, c, default_acc, "click_eval_system", tm, ct)
def api_lambda_1(ip, c, tm, ct):
default_acc = USER_DB["student"][0]
return api_run_student_action(ip, c, default_acc, "click_pre_select_system", tm, ct)
def api_lambda_7(ip, c, acc, tm, ct):
return api_run_student_action(ip, c, acc, "click_leave_system", tm, ct)
def api_lambda_8(ip, c, acc, tm, ct):
return api_run_student_action(ip, c, acc, "click_dorm_system", tm, ct)
def api_lambda_10(ip, c, acc, tm, ct):
return api_run_student_action(ip, c, acc, "click_webmail", tm, ct)
def api_lambda_11(ip, c, acc, tm, ct):
return api_run_student_action(ip, c, acc, "click_vdesk", tm, ct)
def api_lambda_12(ip, c, acc, tm, ct):
return api_run_student_action(ip, c, acc, "malicious_sql_injection", tm, ct)
def test_anomaly(ip, cookie, account, action_name, test_time_str):
role = USER_DB.get(account, ["", "Unknown"])[1]
return record_log(ip, cookie, account, role, action_name, "success", "自訂時間 (模擬過去/未來)", test_time_str)
def toggle_time_input(mode): return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)"))
# --- 4. UI 介面設計 ---
custom_css = """
.mock-panel { background-color: var(--background-fill-secondary); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-color-primary); }
.login-container { max-width: 500px; margin: 0 auto; padding: 20px; }
.ntut-title { color: #d9534f; font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 20px;}
.weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
.headless-tabs > div:first-child { display: none !important; }
.headless-tabs { border: none !important; background: transparent !important; }
.stat-dashboard { display: flex; gap: 15px; margin-bottom: 20px; justify-content: space-between; }
.stat-card { flex: 1; background-color: #ffffff !important; padding: 15px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); display: flex; align-items: center; gap: 15px; border-top: 4px solid transparent; }
.card-total { border-left: 4px solid #10b981; }
.card-anomaly { border-left: 4px solid #ef4444; }
.card-status { border-left: 4px solid #3b82f6; }
.card-icon { font-size: 30px; width: 40px; text-align: center; }
.card-content { flex: 1; }
.card-label { color: #374151 !important; font-size: 13px; margin-bottom: 2px; font-weight: bold !important; }
.card-value-container { display: flex; align-items: baseline; gap: 8px; }
.card-value { font-size: 24px; font-weight: bold; }
.card-total .card-value { color: #000000 !important; }
.card-unit { font-size: 14px; color: #4b5563 !important; font-weight: normal; }
.card-trend { font-size: 12px; padding: 2px 6px; border-radius: 4px; }
.trend-up { background-color: #fee2e2 !important; color: #ef4444 !important; }
.card-sub-text { color: #6b7280 !important; font-size: 11px; margin-top: 2px; }
"""
with gr.Blocks(title="模擬校園入口與資安防護系統") as demo:
store_df_all = gr.State(value=pd.DataFrame(log_history))
store_df_abn = gr.State(value=initial_df_abn)
dashboard_timer = gr.Timer(value=60)
with gr.Row():
with gr.Column(scale=1, elem_classes="mock-panel"):
gr.Markdown("### ⚙️ 環境變數模擬器")
mock_ip = gr.Dropdown(choices=["140.124.71.55 (校內預設)", "140.124.18.22 (宿舍)", "61.228.45.112 (家裡)", "210.61.47.88 (公共場所)", "45.33.22.11 (國外異常IP)"], value="140.124.71.55 (校內預設)", allow_custom_value=True, label="模擬來源 IP")
mock_cookie = gr.Textbox(label="模擬 Cookie Session ID", value="sess_baseline")
gr.Markdown("---")
time_mode = gr.Radio(label="時間設定模式", choices=["真實時間 (目前時間)", "自訂時間 (模擬過去/未來)"], value="真實時間 (目前時間)")
custom_time_input = gr.Textbox(label="輸入自訂時間", placeholder="格式: YYYY-MM-DD HH:MM:SS", value="2026-03-17 03:00:00", visible=False)
gr.Markdown("---")
gr.Markdown("### 📊 即時 Log 與 AI 判定區")
log_display = gr.Dataframe(value=pd.DataFrame(log_history), headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True)
with gr.Column(scale=2):
with gr.Tabs(selected="login_route", elem_classes="headless-tabs") as main_router:
with gr.Tab("Login", id="login_route"):
with gr.Column(elem_classes="login-container"):
gr.Markdown("校園入口網站 Taipei Tech Portal
")
login_msg = gr.Markdown(value="", visible=True)
with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin")
with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234")
with gr.Row(): gr.HTML("F E I K
")
with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)")
login_btn = gr.Button("登入 Login", variant="primary")
gr.Markdown("---")
gr.Markdown("🛠️ 開發與測試專用捷徑
")
with gr.Row():
btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary")
btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary")
with gr.Tab("Dashboard", id="dashboard_route"):
with gr.Column():
with gr.Row():
gr.Markdown("### 資訊系統")
welcome_text = gr.Markdown(value="", elem_classes="text-right")
logout_btn = gr.Button("登出", size="sm")
with gr.Row(): btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary")
gr.Markdown("---")
with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
with gr.Tab("👨🎓 學生專區", id="student_tab"):
with gr.Accordion("▼ 1. 教務系統", open=True):
with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm"); btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm"); btn_course = gr.Button("▶ 課程系統", size="sm")
with gr.Row(): btn_score = gr.Button("▶ 學業成績查詢系統", size="sm")
with gr.Accordion("▼ 2. 學務系統", open=True):
with gr.Row(): btn_leave = gr.Button("▶ 學生請假系統", size="sm"); btn_dorm = gr.Button("▶ 學生宿舍登錄(抽籤)系統", size="sm"); btn_scholarship = gr.Button("▶ 獎助學金申請系統", size="sm")
with gr.Accordion("▼ 3. 資訊服務", open=True):
with gr.Row(): btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm"); btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm")
with gr.Accordion("▼ 惡意操作測試區", open=True):
btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
stat_dashboard_display = gr.HTML(value=get_dashboard_html())
with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"):
action_dropdown = gr.Dropdown(choices=list(ACTION_MAP.keys()), label="選擇要檢視的系統資料分佈", value=None)
action_plot = gr.Plot(show_label=False)
with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
gr.Markdown("動態防護已全面啟動:包含 HDBSCAN 空間異常偵測、IP 聲譽聯防,以及動態視窗頻率攔截。")
with gr.Row():
overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100)
time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100)
ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
with gr.Row():
device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
geo_weight = gr.Number(label="🌍 地理偏移敏感度 (%)", value=50, minimum=0, maximum=100)
freq_weight = gr.Number(label="⚡ 行為頻率敏感度 (%)", value=60, minimum=0, maximum=100)
weights_status = gr.Textbox(value="", visible=False)
with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"):
abnormal_log_display = gr.Dataframe(value=initial_df_abn, headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True)
with gr.Row():
abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入...", scale=4)
btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1)
ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False)
with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
gr.Markdown("點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。")
gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)")
with gr.Row():
t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
with gr.Row():
t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
with gr.Row():
t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm")
gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
with gr.Row():
t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm"); t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm")
with gr.Row():
t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm"); t_score_anom = gr.Button("⚠️ 異常: 學業成績查詢系統 (02-15 03:00)", size="sm")
gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
with gr.Row():
t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm")
with gr.Row():
t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm")
with gr.Row():
t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)")
with gr.Row():
t_istudy_norm = gr.Button("✅ 正常: 北科i學園PLUS (10-18 16:00)", size="sm"); t_istudy_anom = gr.Button("⚠️ 異常: 北科i學園PLUS (08-20 02:00)", size="sm")
with gr.Row():
t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm")
with gr.Row():
t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
def update_dataframes(df_all, df_abn): return gr.update(value=df_all), gr.update(value=df_abn)
store_df_all.change(fn=update_dataframes, inputs=[store_df_all, store_df_abn], outputs=[log_display, abnormal_log_display])
demo.load(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
dashboard_timer.tick(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
action_dropdown.change(fn=get_plot_data, inputs=[action_dropdown], outputs=[action_plot], api_name="getPlotData")
time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
# weight_inputs = [overall_weight, time_weight, ip_weight, device_weight, geo_weight, freq_weight]
# 移除所有 UI 欄位的 update_system_weights 綁定,僅保留 API 專用端點
update_outputs = [main_router, role_tabs, login_msg, welcome_text, store_df_all, store_df_abn, abnormal_log_input]
login_btn.click(fn=process_login, inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input], outputs=update_outputs)
logout_btn.click(fn=logout, inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input], outputs=[main_router, acc_input, pwd_input, captcha_input, login_msg, store_df_all, store_df_abn, abnormal_log_input])
fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input]
fast_outputs = update_outputs + [acc_input]
btn_fast_student.click(
fn=lambda ip, c, tm, ct: fast_login("student", ip, c, tm, ct),
inputs=fast_inputs,
outputs=fast_outputs,
api_name=None
)
btn_fast_admin.click(
fn=lambda ip, c, tm, ct: fast_login("admin", ip, c, tm, ct),
inputs=fast_inputs,
outputs=fast_outputs,
api_name=None
)
btn_toggle_role.click(fn=toggle_role, inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input], outputs=[role_tabs, welcome_text, store_df_all, store_df_abn, abnormal_log_input, acc_input])
action_mapping = [
(btn_eval, "click_eval_system"), (btn_pre_select, "click_pre_select_system"), (btn_add_drop, "click_add_drop_system"),
(btn_istudy, "click_istudy"), (btn_card, "click_card_loss"), (btn_course, "click_course_system"),
(btn_score, "click_score_query"), (btn_leave, "click_leave_system"), (btn_dorm, "click_dorm_system"),
(btn_scholarship, "click_scholarship"), (btn_webmail, "click_webmail"), (btn_vdesk, "click_vdesk"),
(btn_hack_score, "malicious_sql_injection")
]
for btn, action_name in action_mapping:
btn.click(
fn=lambda ip, c, acc, tm, ct, n=action_name: simulate_click(ip, c, acc, n, tm, ct),
inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input],
outputs=[store_df_all, store_df_abn, abnormal_log_input],
api_name=None
)
test_mapping = [
(t_pre_sel_norm, "click_pre_select_system", "2026-06-13 11:00:00"), (t_pre_sel_anom, "click_pre_select_system", "2026-10-15 13:00:00"),
(t_course_norm, "click_course_system", "2026-02-25 07:50:00"), (t_course_anom, "click_course_system", "2026-07-15 05:00:00"),
(t_add_drop_norm, "click_add_drop_system", "2026-02-18 14:00:00"), (t_add_drop_anom, "click_add_drop_system", "2026-05-15 12:00:00"),
(t_eval_norm, "click_eval_system", "2026-12-17 13:20:00"), (t_eval_anom, "click_eval_system", "2026-03-15 03:00:00"),
(t_score_norm, "click_score_query", "2026-11-17 18:50:00"), (t_score_anom, "click_score_query", "2026-02-15 03:00:00")
]
for btn, action_name, test_time_str in test_mapping:
btn.click(
fn=lambda ip, c, acc, n=action_name, t_str=test_time_str: test_anomaly(ip, c, acc, n, t_str),
inputs=[mock_ip, mock_cookie, acc_input],
outputs=[store_df_all, store_df_abn, abnormal_log_input],
api_name=None
)
# =====================================================================
# 👉 新增:隱藏的 UI 綁定區 (讓外部能透過 api_name 存取資料)
# =====================================================================
with gr.Group(visible=False):
api_stats_btn = gr.Button("API_Stats")
api_stats_out = gr.JSON()
api_stats_btn.click(fn=api_get_stats, inputs=[], outputs=[api_stats_out], api_name="get_stats")
api_logs_btn = gr.Button("API_Logs")
api_logs_in = gr.Number(value=20)
api_logs_out = gr.JSON()
api_logs_btn.click(fn=api_get_logs, inputs=[api_logs_in], outputs=[api_logs_out], api_name="get_logs")
api_chart_btn = gr.Button("API_Chart")
api_chart_in = gr.Textbox()
api_chart_out = gr.JSON()
api_chart_btn.click(fn=api_get_chart_data, inputs=[api_chart_in], outputs=[api_chart_out], api_name="get_chart_data")
api_abnormal_logs_btn = gr.Button("API_Abnormal_Logs")
api_abnormal_logs_out = gr.JSON()
api_abnormal_logs_btn.click(fn=api_get_abnormal_logs, inputs=[], outputs=[api_abnormal_logs_out], api_name="get_abnormal_logs")
api_plot_btn = gr.Button("API_Plot")
api_plot_in = gr.Textbox()
api_plot_out = gr.Textbox()
api_plot_btn.click(fn=get_plot_data_for_api, inputs=[api_plot_in], outputs=[api_plot_out], api_name="get_plot_data_api")
# System weights update API endpoint
api_update_weights_btn = gr.Button("API_Update_Weights")
api_update_weights_ow = gr.Number(label="Overall Weight")
api_update_weights_tw = gr.Number(label="Time Weight")
api_update_weights_iw = gr.Number(label="IP Weight")
api_update_weights_dw = gr.Number(label="Device Weight")
api_update_weights_gw = gr.Number(label="Geo Weight")
api_update_weights_fw = gr.Number(label="Frequency Weight")
api_update_weights_in = [api_update_weights_ow, api_update_weights_tw, api_update_weights_iw, api_update_weights_dw, api_update_weights_gw, api_update_weights_fw]
api_update_weights_out = gr.Textbox()
api_update_weights_btn.click(fn=update_system_weights, inputs=api_update_weights_in, outputs=[api_update_weights_out], api_name="update_system_weights")
# ================================================================
# Vue student actions API endpoints
# - 對應 Digital-Bodyguard-Fontend/src/App.vue 裡的 ACTION_ENDPOINTS
# - 對應 Digital-Bodyguard-Fontend/src/services/gradioService.ts 的 lambda*
# ================================================================
api_student_ip = gr.Textbox(label="ip")
api_student_cookie = gr.Textbox(label="c")
api_student_time_mode = gr.Radio(label="tm", choices=["真實時間 (目前時間)", "自訂時間 (模擬過去/未來)"], value="真實時間 (目前時間)")
api_student_custom_time = gr.Textbox(label="ct")
# lambda (期末教學評量) => api_name="lambda"
api_lambda_out = gr.Textbox()
gr.Button("API_lambda", visible=False).click(
fn=api_lambda,
inputs=[api_student_ip, api_student_cookie, api_student_time_mode, api_student_custom_time],
outputs=[api_lambda_out],
api_name="lambda",
)
# lambda_1 (預選系統)
api_lambda_1_out = gr.Textbox()
gr.Button("API_lambda_1", visible=False).click(
fn=api_lambda_1,
inputs=[api_student_ip, api_student_cookie, api_student_time_mode, api_student_custom_time],
outputs=[api_lambda_1_out],
api_name="lambda_1",
)
# lambda_7 (請假系統)
api_lambda_acc = gr.Textbox(label="acc")
api_lambda_7_out = gr.Textbox()
gr.Button("API_lambda_7", visible=False).click(
fn=api_lambda_7,
inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time],
outputs=[api_lambda_7_out],
api_name="lambda_7",
)
# lambda_8 (宿舍登錄)
api_lambda_8_out = gr.Textbox()
gr.Button("API_lambda_8", visible=False).click(
fn=api_lambda_8,
inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time],
outputs=[api_lambda_8_out],
api_name="lambda_8",
)
# lambda_10 (網路郵局)
api_lambda_10_out = gr.Textbox()
gr.Button("API_lambda_10", visible=False).click(
fn=api_lambda_10,
inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time],
outputs=[api_lambda_10_out],
api_name="lambda_10",
)
# lambda_11 (軟體雲)
api_lambda_11_out = gr.Textbox()
gr.Button("API_lambda_11", visible=False).click(
fn=api_lambda_11,
inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time],
outputs=[api_lambda_11_out],
api_name="lambda_11",
)
# lambda_12 (SQL Injection)
api_lambda_12_out = gr.Textbox()
gr.Button("API_lambda_12", visible=False).click(
fn=api_lambda_12,
inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time],
outputs=[api_lambda_12_out],
api_name="lambda_12",
)
# ================================================================
# Generic student action API (Vue 一次呼叫任意系統按鈕)
# ================================================================
api_student_action_code = gr.Textbox(label="action_code")
api_student_run_out = gr.Textbox()
gr.Button("API_RunStudentAction", visible=False).click(
fn=api_run_student_action_code,
inputs=[
api_student_ip,
api_student_cookie,
api_lambda_acc,
api_student_action_code,
api_student_time_mode,
api_student_custom_time,
],
outputs=[store_df_all, store_df_abn, api_student_run_out],
api_name="run_student_action",
)
# =====================================================================
if __name__ == "__main__":
demo.launch(css=custom_css)