WebCoding.md
WebCoding.md — RenUniversal Web 後端
這是 AI Coding Agent 讀的啟動規格檔(bootstrap spec)。開始動任何 Python / HTML / JS 程式碼前,先完整讀完本檔。本檔的決策是鎖定的,不得因「更好的想法」而擅自更動;若你認為某決策有誤,先停下來提出質疑,不要直接改。
0. 你是誰、你在做什麼
你是本專案 Web 後端的實作 agent。RenUniversal 的 Python 後端(backend/)是讓桌機 / 任意平台零安裝執行姿態監控的主要路徑——使用者不需要 iPhone,只需要 make run 即可取得完整功能。
本端的核心職責:
backend/stream_server.py(Flask):接收瀏覽器與手機的 HTTP 請求,提供即時 MJPEG 影像串流、SSE 狀態推送,以及所有 REST API。backend/core/pipeline.py(AgentPipeline):30 FPS 驅動相機擷取 → MediaPipe 推理 → 技能/事件評估 → SharedState 更新。整個流程跑在獨立的背景 Thread,與 Flask 的請求線程完全分離。backend/core/action_engine.py(ActionEngine):掃描skills/目錄,動態載入每個技能包,每幀呼叫evaluate_all()。backend/core/event_engine.py(EventEngine):掃描events/目錄,使用safe_eval.py的 AST 白名單布林求值器,把技能狀態組合成複合事件。web/(Jinja2 模板 + Tailwind CDN):所有前端頁面。
不要動 iOS Swift 程式碼(RenUniversal/RenUniversal/)。除非任務明確指定,你只操作 backend/、web/、skills/、events/ 目錄。
1. 開發方法
工作循環固定為:
- 讀規格 — 讀本檔對應章節,確認要做的功能屬於哪個 phase、是否在當前範圍內。
- 確認契約 — 你動的模組必須符合第 4 節的型別契約(Pydantic schema)。Schema 優先於實作;不得為了實作方便偷改 Pydantic 模型。
- 最小切片 — 一次只實作一個可驗證的最小功能,能跑、能測。
- 驗證 — 每個功能都要有對應的手動驗證方式(第 6 節驗收標準)。
- 停在 gate — 到達 phase gate 時停下來回報,不要自動往下一個 phase 衝。
若某功能不在當前 phase 範圍,就算你覺得「順手加一下」很合理,也不准建。
2. 技術選型(已鎖定)
| 層 | 選型 | 為何 |
|---|---|---|
| Web 框架 | Flask 3.x(threaded=True) |
同步 API 與 MJPEG 串流用 Thread 隔離效果最佳;FastAPI 的 async 對本專案的 blocking I/O(cv2、MediaPipe)沒有優勢(見 §9.1) |
| 視覺推理 | MediaPipe Python SDK(mediapipe>=0.10.0) |
跨平台、Face Mesh 468 點 + Pose 33 點同一個推理迴圈 |
| 資料驗證 | Pydantic v2(BaseModel、Field) |
所有 API request/response 有型別保障;safe_eval.py 不需要 Pydantic 但整體生態統一 |
| 影像處理 | OpenCV 4.x | MJPEG encode、臉部馬賽克、帧疊加、相機讀取 |
| 日誌 | Loguru(from loguru import logger) |
比 stdlib logging 更少樣板、彩色輸出、結構化 |
| 前端 | Jinja2 模板 + Tailwind CSS CDN(tailwind.js,內建於 web/) |
零前端工具鏈,修一個 HTML 存檔即生效 |
| 內網穿透 | localhost.run(SSH tunnel,--enable-tunnel) |
零安裝,ssh -R 80:localhost:8080 [email protected] 一行完成 |
| Python 版本 | 3.9 ≤ x ≤ 3.12(不支援 3.13+) | MediaPipe wheel 目前只到 3.12(見 §10 坑 1) |
不要引入任何非 requirements.txt 的 Python 套件,除非使用者明確要求並討論過相容性。
3. 系統架構設計原則
3.1 Threading 模型
主執行緒 (Flask + Werkzeug)
├── GET / → render_template(monitor.html)
├── GET /live → generate_mjpeg_stream() [blocking generator]
├── GET /api/status/stream → SSE generator
├── POST /api/skills/create → create_skill()
└── ... 其餘 REST 端點
背景執行緒 (daemon)
└── capture_loop()
└── AgentPipeline.run_cycle() ← 唯一讀寫相機與 SharedState 的地方
├── VideoCaptureSkill.get_frame()
├── MediaPipe FaceLandmarker + PoseLandmarker
├── ActionEngine.evaluate_all()
├── EventEngine.evaluate_all()
└── SharedState.update_frame() / update_status()
所有 SharedState 的讀寫都有 threading.Lock。Flask 請求線程讀 state 時走 state.get_status()(取 lock 後回傳副本),不直接操作內部欄位。違反此原則會導致競態條件(race condition),在高 FPS 下難以復現、難以除錯。
3.2 Plugin 架構(Skills & Events)
skills/ events/
slouch/ bad_posture/
config.json config.json ← rule_syntax: "lean OR turn OR tilt"
lean/
config.json
lean_custom/
config.json
logic.py ← 有 logic.py → 載入 ActionDetector class
- 無程式碼路徑:只有
config.json,ActionEngine自動分配GenericActionDetector,依rule_syntax欄位的點位距離語法判定。 - 進階程式碼路徑:有
config.json+logic.py,ActionEngine動態importlib.util.spec_from_file_location載入,實例化ActionDetector(config)class。 - 熱載入:新增、修改、刪除技能後,呼叫
pipeline.action_engine.load_action_skills()即可重載,不需要重啟伺服器。Events 同理,呼叫pipeline.event_engine.reload()。 - 名稱安全化:API 接收到的技能名稱一律過
"".join([c for c in name if c.isalnum() or c in ('_', '-')]).lower(),再用來建立資料夾,防止路徑穿越(path traversal)。
3.3 SharedState 是唯一真相來源
# backend/core/state.py (已實作)
state.get_status() → 讀狀態(thread-safe,回傳 DetectorStatus 副本)
state.update_status(...) → 寫狀態(thread-safe)
state.get_frame() → 讀最新標注影格(thread-safe)
state.update_frame(frame) → 寫最新影格(thread-safe)
state.save_prefs(dict) → 寫使用者偏好(thread-safe,同步持久化到 preferences.json)
state.update_network_frame(frame, source_id) → 接收手機/網路副鏡頭影格
禁止在 capture_loop() 以外的地方直接呼叫 pipeline.run_cycle(),也禁止直接讀取 pipeline 的成員變數作為狀態來源——全部透過 state 取得。
3.4 MJPEG 串流與 SSE 的不同用途
| 路由 | 格式 | 用途 |
|---|---|---|
/live 或 /video_feed |
MJPEG (multipart/x-mixed-replace) |
實時相機畫面顯示於 <img> 標籤 |
/api/status/stream |
SSE (text/event-stream) |
低頻率(10 Hz)推送 JSON 狀態,驅動 Web UI 的數值更新 |
/status |
JSON REST | 單次查詢完整狀態(含 prefs、local_ip) |
不要讓 MJPEG 串流做狀態同步,也不要讓 SSE 夾帶影格資料。兩條路徑各司其職,混合會破壞瀏覽器端的解碼效率。
4. 介面契約(鎖定,實作必須符合)
4.1 DetectorStatus(backend/core/schema.py)
class DetectorStatus(BaseModel):
ratio: float # 鼻下巴比率(百分比)
nose_chin_ratio: float # 原始比率
is_bad_posture: bool
down_count: int
fps: int
connected: bool
calibrating: bool
calibration_progress: int # 0–100
is_turning: bool
baseline_eye_dist: float
threshold: float # 百分比單位(UI 顯示)
yaw_tolerance: float # 百分比單位(UI 顯示)
sway_threshold: float
lean_threshold: float
is_active: bool
latency_ms: int
is_swaying: bool
is_leaning_forward: bool
sway_ratio: float
lean_ratio: float
camera_source: Union[str, List[str]]
public_url: Optional[str]
flip_enabled: bool
privacy_mode: bool
active_skills: dict # {skill_name: bool}
active_events: dict # {event_name: bool}
metrics: dict # {skill_name: float}(比率數值,供 UI 顯示)
trigger_counts: dict # {name: int}
不得移除任何欄位。前端 JS(web/js/monitor.js、各 HTML 內的 <script> 區塊)直接解構這個 JSON,移除欄位等同在前端撒謷。新增欄位必須給預設值,確保舊版前端不會因找不到 key 而 crash。
4.2 SettingsUpdate 與 ControlCommand
class SettingsUpdate(BaseModel):
threshold: Optional[float]
yaw_tolerance: Optional[float]
sway_threshold: Optional[float]
lean_threshold: Optional[float]
camera_source: Optional[Union[str, List[str]]]
flip_enabled: Optional[bool]
privacy_mode: Optional[bool]
class ControlCommand(BaseModel):
active: bool
SettingsUpdate 的所有欄位都是 Optional,伺服器只更新有出現的欄位。不要把非 Optional 欄位加進來,會讓只想更新一個設定的 API 呼叫變成必填整包。
4.3 Skill config.json 結構
{
"name": "slouch",
"description": "人類可讀說明",
"enabled": true,
"requirements": {
"face_mesh": true,
"pose": false
},
"rule_syntax": "f1,f152 >< num=20%",
"default_preferences": {
"slouch_threshold": 0.2
}
}
rule_syntax 是 GenericActionDetector 讀取的唯一判定來源。舊版有一個 rules: [] list 欄位(已棄用),程式碼有向後相容處理(若 rule_syntax 為空則從 rules[0] fallback),但新建技能一律用 rule_syntax 欄位,不要再寫 rules。
4.4 Event config.json 結構
{
"name": "bad_posture",
"description": "人類可讀說明",
"enabled": true,
"rule_syntax": "lean OR turn OR tilt",
"rules": ["lean OR turn OR tilt"]
}
rule_syntax 是 EventEngine 讀取的布林運算式,由 safe_eval.py 求值。合法語法:技能名稱、AND、OR、NOT(!)、括號。禁止在 event rule_syntax 裡寫點位距離語法(f1,f152 >< ...),那是 skill 層的格式;event 層只能引用已存在的 skill 名稱。
5. 目錄結構
backend/
stream_server.py # Flask App 進入點:所有路由、auth、MJPEG、SSE、skill/event CRUD API
core/
pipeline.py # AgentPipeline:30 FPS 主驅動迴圈(背景 Thread)
action_engine.py # ActionEngine:掃描 skills/,熱載入,evaluate_all()
event_engine.py # EventEngine:掃描 events/,safe_eval 布林求值
generic_skill_detector.py # GenericActionDetector:解析 rule_syntax 的無程式碼偵測器
state.py # SharedState:thread-safe 狀態中心 + 偏好持久化
schema.py # Pydantic 模型:DetectorStatus / SettingsUpdate / ControlCommand
safe_eval.py # AST 白名單布林求值器(取代 eval(),防 RCE)
landmarks.py # get_landmark_coord():f<n>/p<n> 點位 ID → 像素座標
skill_template.py # 進階 logic.py 可繼承的基底,提供預計算特徵字串
models/
face_landmarker.task # MediaPipe Face Landmarker 模型(不進版控,首次 setup 下載)
pose_landmarker_lite.task # MediaPipe Pose Landmarker 模型(同上)
services/
calibration_wizard/ # CalibrationWizardSkill:30 幀採樣 + 基準計算
video_capture/ # VideoCaptureSkill:相機讀取(本地 + 網路手機串流)
requirements.txt
web/
base.html # 所有頁面繼承的 Jinja2 base(nav、頭部、Tailwind)
monitor.html # 主監控頁(即時 MJPEG + SSE 狀態儀表板)
camera.html # 相機設定(切換鏡頭、隱私模式、翻轉)
skills.html # 技能管理(新增、編輯、啟用/停用、刪除)
events.html # 事件管理(複合規則)
apps_launcher.html # 網頁版 App 啟動器
mpmap.html # MediaPipe 地標點選工具(500+ 點位互動預覽)
MobileCamera.html # 手機網路副鏡頭頁(UserMedia → POST /upload_frame)
js/
monitor.js # 監控頁的 SSE 監聽 + DOM 更新邏輯
tailwind.js # Tailwind CSS CDN 本地快取(offline 可用)
apps/
Game.html # 內建遊戲 App
Statistics.html # 統計工具 App
skills/ # 技能插件目錄(每個子目錄是一個技能包)
slouch/config.json
lean/config.json
turn/config.json
tilt/config.json
<custom>/config.json # UI 建立的技能
<custom>/logic.py # 進階技能(可選)
events/ # 複合事件目錄
bad_posture/config.json
ctar_tuck/config.json
<custom>/config.json
preferences.json # 使用者偏好(gitignored,runtime 自動建立)
start.py # 一鍵啟動包裝腳本(自動偵測 venv python)
Makefile # make setup / make run / make stop / make doctor
6. 建構順序與驗收標準
依序做,每步做完自我驗證再往下:
- 環境建置 —
make setup(或python3 -m venv .venv && pip install -r requirements.txt)能跑完無 error。MediaPipe models 能從make setup下載到backend/models/。驗收:make run後能在 http://127.0.0.1:8080 看到監控頁。 - 相機串流 — MJPEG stream 在
/live端點順暢,不卡頓、無明顯延遲。驗收:瀏覽器開 monitor.html,能看到即時相機畫面與 FPS 顯示 ≥ 25。 - 校準流程 —
/recalibratePOST 後,calibrating: true→ 幾秒後calibrating: false, calibration_progress: 100。驗收:網頁校準按鈕後狀態列顯示「校準完成」。 - 技能評估 — 內建
slouch/lean/turn/tilt四個技能,active_skills在/status的 JSON 裡出現且能正確觸發與解除。驗收:低頭時slouch: true,回正後false;在skills.html能看到四個技能的即時狀態切換。 - 事件評估 —
bad_posture(OR)與ctar_tuck(slouch AND NOT turn AND NOT lean)正確計算。驗收:lean單獨觸發時bad_posture: true;slouch亮、turn/lean都滅時ctar_tuck: true。 - 技能 CRUD API —
POST /api/skills/create、/toggle、/delete正確操作skills/目錄,並在回應後立即觸發load_action_skills()熱載入。驗收:在skills.html建立一個新技能 → 即時出現在監控頁的觸發狀態列表,不需要重啟。 - 手機副鏡頭 — 手機開啟
/mobile,瀏覽器要求相機權限後持續 POST/upload_frame,主機端 pipeline 切換camera_source=phone後能正確讀取手機影格做推理。驗收:在不同 Wi-Fi 設備上(需 LAN 可達)能看到主機監控頁的鏡頭切換到手機畫面。 - 隱私模式 —
privacy_mode: true時,MJPEG 串流中的人臉區域被高斯模糊(包含 150% 外擴邊界),但/status仍然正確回報姿態觸發狀態(判定不受模糊影響)。驗收:在 camera.html 切換隱私模式,MJPEG 串流即時反映模糊/不模糊。 - Basic Auth —
make run -- --host 0.0.0.0時,若未提供--auth,伺服器應自動產生帳密並顯示在 log,未認證的請求回 401。驗收:無--auth啟動,瀏覽器直接輸入 IP 出現 Basic Auth 對話框;帶--auth user:pass啟動則直接放行。 --cli無頭模式 —python backend/stream_server.py --cli能跑起來,觸發狀態變化時自動 print JSON 到 stdout。驗收:python backend/stream_server.py --cli | head -20在低頭後看到含"triggered"的 JSON 輸出。
7. 程式碼規範
- 所有 API 寫入操作都走
get_json_or_400(),不得用裸request.get_json()(原因見 §10 坑 3)。 - 事件引擎的規則求值一律走
safe_eval_bool(),禁止任何形式的eval()(原因見 §9.3)。 - 技能資料夾名稱安全化:所有從 API 接收的
name在建立資料夾前都要過"".join([c for c in name if c.isalnum() or c in ('_', '-')]).lower(),並驗證safe_name非空。 - SharedState 讀寫只透過公開方法(
get_status()、update_status()、get_frame()、update_frame()、save_prefs()),禁止在 Flask 路由函數裡直接操作state.status或state.frame。 - 日誌用 loguru(
from loguru import logger),不要混用print()或logging.getLogger()。Pipeline 的日常 frame log 層級用logger.debug,正常人不需要看到的資訊不要用logger.info(會噪)。 preferences.json的數值有雙單位系統(見 §10 坑 2),在save_prefs()已統一處理轉換,Flask 路由不要自己做單位轉換。
8. 反腐化紅線(違反即回報,不得自作主張)
- 在 Flask 路由函數裡直接讀寫
pipeline的 private 成員(pipeline.is_calibrated、pipeline.baseline_eye_distance等)取得狀態——這些欄位只有capture_loop()Thread 持有,在路由 Thread 讀它們是未加 lock 的競態讀取。 - 在 event
rule_syntax裡寫點位距離語法(如f1,f152 >< num=20%)——EventEngine 不會解析這個格式,safe_eval_bool()會拒絕任何非True/False/AND/OR/NOT/括號的內容。 - 使用
eval()或exec()求值任何來自使用者輸入的字串(包含 rule_syntax、技能名稱、設定值)。 - 在
capture_loop()背景 Thread 以外呼叫pipeline.run_cycle()。 - 在沒有
state.frame_lock保護的情況下操作state.frame。 - 引入任何
requirements.txt以外的 Python 套件,不含明確討論。 - 直接把
preferences.json路徑硬寫成絕對路徑。SharedState預設prefs_path="preferences.json"(相對 CWD),其他地方同理,保持相對路徑。
9. 歷史決策紀錄
9.1 為何選 Flask 而非 FastAPI(定案)
MediaPipe 的 Python API 是同步 blocking call,每幀推理需要 10–30 ms。若用 FastAPI(asyncio 架構),就不得不把推理丟到 thread pool executor(asyncio.run_in_executor),再把結果送回 async 上下文——等同把 Flask 的 Thread 模型包一層 async 皮,複雜度上升而沒有任何吞吐量增益。本專案同時線上的連線數極少(1–3 人),Flask 的 threaded=True 已完全足夠。FastAPI 的 Pydantic 整合優勢在這裡可以直接在 Flask 裡也用 Pydantic,沒有任何東西非 FastAPI 不可。
9.2 capture_loop() 為何要跑在獨立 Thread 而非 Flask 請求週期內
MJPEG 的 generator 函數(generate_mjpeg_stream())是在 Flask 請求的線程裡執行的,它直接從 state.get_frame() 讀已處理好的影格,每幀只需 30 ms 的等待(time.sleep(0.033))。若改成「request 觸發 → 同步推理 → 回傳」,一個 MJPEG 連線會佔用一個 Thread 做全部的 MediaPipe 推理,兩個瀏覽器標籤同時開 /live 就要跑兩份推理,CPU 翻倍;現在的設計無論幾個瀏覽器標籤,推理只跑一份。
9.3 safe_eval.py:為何不用 eval() 求值事件規則(資安決策)
事件規則(lean OR turn AND NOT tilt)必須支援使用者透過 Web UI 或 API 自由輸入。若用 eval() 直接執行,攻擊者(或使用者自己不小心)可以輸入 __import__('os').system('rm -rf /') 之類的 payload,在伺服器上執行任意指令(RCE)。
safe_eval.py 的做法:
- 先把所有技能名稱(
lean、turn...)替換為True或False字面值。 - 用
ast.parse(expr, mode="eval")產生 AST。 - 遍歷 AST,白名單只允許
BoolOp / UnaryOp / And / Or / Not / Constant / Load,任何其他 AST 節點(Call、Attribute、Name、BinOp等)一律 raiseValueError,呼叫端捕捉後回傳False(安全失敗)。
這是本專案最重要的資安邊界,不得繞過。
9.4 網路來源 TTL 驅逐與 16 來源上限(資安決策)
/upload_frame 接受含 ?source=xxx 的手機副鏡頭推送,state.update_network_frame(frame, source_id) 會把 source_id 作為 key 存在 network_frames dict 裡。若不加限制,攻擊者可以用無限多個 source_id 灌滿記憶體(DoS)。
防護機制(已實作於 state.py):
- TTL 驅逐:每次寫入時,掃描並移除超過 10 秒未更新的 source。
- 16 源上限:若 source 數量已達 16,淘汰最舊的一筆(按 timestamp 排序)。
- 正常使用者最多只有 1–2 個手機連線,這些限制對正常使用沒有任何影響。
9.5 Basic Auth 自動產生邏輯(資安決策)
當使用者以 --host 0.0.0.0 或 --enable-tunnel 讓服務對外可達,卻忘記設定 --auth,裸奔的服務會讓任何人都能看到相機畫面。
因此:若偵測到 host != '127.0.0.1' 或 enable_tunnel 為 true,且沒有明確的 --auth,stream_server.py 會用 secrets.choice() 生成 12 字元隨機密碼(大小寫字母 + 數字),強制啟用 Basic Auth,並把帳密顯示在 log 裡。使用者看到 log 就能取得存取方式,不會被鎖死。
不要移除這個自動保護機制,即使某些使用者覺得「麻煩」。
9.6 隱私打碼的 150% 外擴邊界(使用者體驗決策)
臉部馬賽克若只蓋臉部追蹤點的精確邊界框,會漏掉頭髮邊緣、耳朵、脖子,反而顯得不完整又詭異。測試後定案以 face_bbox * 1.5 為打碼區域。代價是偶爾會蓋到背景區域,但使用者普遍認為「過度遮蓋比遮不住好」。
實作位於 backend/core/pipeline.py 的 privacy blur 區段,使用 cv2.GaussianBlur()。不要改成精確邊界框,那是退化,不是優化。
9.7 --cli 無頭模式設計
臨床場景有時不需要 Web 介面,只需要「觸發狀態改變時輸出 JSON 到 stdout,讓其他程式(HMI 軟體、串列埠橋接器)用 pipe 讀取」。--cli flag 讓 capture_loop() 跑在主線程,不啟動 Flask,觸發狀態集合(current_triggered)變化時才輸出一行 JSON。
這個 flag 的使用情境是整合進其他系統而非互動式使用,不需要為它加複雜的配置。
10. 已知坑與變通方案
坑 1:MediaPipe wheel 不支援 Python 3.13+
MediaPipe 官方 wheel 截至目前(2026 年 7 月)最高支援 Python 3.12。Makefile 的 Python 版本掃描已把 3.13+ 排除在候選清單外(只掃 3.9–3.12)。若使用者的系統預設 python3 是 3.13(macOS Sequoia 預設),make setup 會找到 Homebrew 安裝的 3.12 並使用它。
若使用者環境完全沒有 3.9–3.12,make doctor 會顯示錯誤並提示安裝 Homebrew [email protected]。不要嘗試讓 MediaPipe 在 3.13 上跑,沒有 wheel、從源碼編譯在 Apple Silicon 上很容易出問題。
坑 2:preferences.json 的數值有雙單位(% 和 ratio)
DetectorStatus(前端顯示)使用百分比單位(threshold: 20.0 代表 20%),但 preferences.json(持久化)使用0.0–1.0 的小數 ratio(threshold_ratio: 0.20)。SharedState.save_prefs() 有雙向轉換邏輯,Flask 路由傳入的 SettingsUpdate.threshold 是百分比,存到 prefs 時自動除以 100。
典型的錯誤是在路由或 pipeline 裡自己做一次 / 100.0,導致 threshold 變成原來的 1/100。所有單位轉換都在 save_prefs() 裡統一處理,其他地方一律傳百分比(與 DetectorStatus 一致)。
坑 3:裸 request.get_json() 在空 body 時返回 None 導致 500
Flask 的 request.get_json() 在以下情況回傳 None:
- Content-Type 不是
application/json - Body 是空的
- Body 是非合法 JSON
若後面直接 data['key'] 就會 TypeError: 'NoneType' object is not subscriptable,Flask 把它當 500 回給前端。正確做法是所有寫入 API 統一用已實作的 get_json_or_400() helper:
def get_json_or_400():
data = request.get_json(silent=True)
if not isinstance(data, dict):
return None, (jsonify({"error": "Request body must be a valid JSON object"}), 400)
return data, None
坑 4:skills/ 路徑是相對 CWD,不是相對 stream_server.py
ActionEngine(skills_dir="skills") 和 EventEngine(events_dir="events") 使用相對路徑,解析基準是啟動 process 時的工作目錄(CWD)。
- 用
make run(在 project root 執行)→ CWD 是 project root →skills/找得到 ✓ - 用
cd backend && python stream_server.py→ CWD 是backend/→skills/在backend/skills/,根本不存在 ✗
正確啟動方式永遠是在 project root(python start.py 或 make run 或 python backend/stream_server.py),不要 cd backend/ 後啟動。若需要修正,PROJECT_ROOT 在 stream_server.py 最上方已定義,技能路徑應改用 os.path.join(PROJECT_ROOT, 'skills') 的絕對路徑形式。
坑 5:--enable-tunnel(localhost.run)連線是公網可達的
--enable-tunnel 透過 SSH Reverse Tunnel 把 8080 port 曝露到 *.lhr.life 的公開網址。這意味著任何人都可以連這個網址,不只是同一個 Wi-Fi 上的人。
這個 flag 的設計是短暫使用(例如:跨 Wi-Fi 的臨床測試場景),不是長期開著。Auto Basic Auth 機制(§9.5)保護了這個端點,但連線本身是未加密的 HTTP(8080),Basic Auth 帳密在 LAN 以外傳輸是明文。若需要加密,改用 HTTPS(8443,self-signed)。
11. 啟動與部署快速參考
# 一般使用(僅本機)
make run
# 或
python start.py
# 手機副鏡頭(區網可達)
python backend/stream_server.py --host 0.0.0.0 --port 8080
# 系統自動產生 Basic Auth 帳密,看 log
# 自訂 Basic Auth
python backend/stream_server.py --host 0.0.0.0 --auth admin:mysecretpass
# 跨 Wi-Fi 存取(公網 tunnel,謹慎使用)
python backend/stream_server.py --enable-tunnel
# 關閉預設隱私模式(展示、測試用途)
python backend/stream_server.py --disable-privacy
# 無頭 CLI 模式(串接外部程式)
python backend/stream_server.py --cli | jq '.'
# 環境診斷
make doctor
HTTPS 雙埠:伺服器啟動後同時在 8080(HTTP)和 8443(HTTPS,self-signed)監聽。手機瀏覽器的 getUserMedia(相機 API)需要 Secure Context,必須走 8443。首次連線需要在手機上信任自簽憑證。
12. 待規劃(尚未排入)
以下功能使用者曾提及但尚未進入開發,不要提前做:
- ❌ WebSocket 即時雙向通訊:目前 SSE 是單向推送;若未來要前端發指令(如:即時觸發校準),需要改成 WebSocket 或在現有 REST 上加長輪詢。
- ❌ 多使用者同時校準:目前校準基準是全局的(
pipeline.baseline_*),多人同時連線共用同一組基準值——這在臨床上不正確,但改成 per-session 基準需要 session 機制。 - ❌ 技能
logic.py線上編輯 UI:目前進階技能需要手動建立logic.py並放到skills/<name>/,Web UI 只支援無程式碼的 rule_syntax 技能。線上程式碼編輯器(如 CodeMirror)可以提供更好的體驗,但涉及在瀏覽器裡寫 Python 並執行的安全問題。 - ❌ 事件 → 外部 Webhook 觸發:目前觸發狀態只顯示在 Web UI,若要讓外部系統(HMI、IoT)接收,需要
/api/webhooksCRUD + 觸發時的 HTTP POST。 - ❌ 歷史數據持久化到 SQLite:目前
trigger_counts存在記憶體裡,重啟後歸零;down_count也是。若要做跨 session 的歷史趨勢圖,需要 SQLite 或 CSV 落地。
本檔撰於 2026 年 7 月,版本 v1.3.4。