# 麟曜數位工作室 完整文件
# 快速開始
HUAN 讙讓你在瀏覽器裡設計一面畫面,然後把它送到現場的螢幕上。
HUAN 讙讓你在瀏覽器裡設計一面畫面,然後把它送到現場的螢幕上。
這一頁帶你走完一次完整流程:從登入到第一面看板亮起來。
:::tip[這一份是給使用者的]
如果你要架設 HUAN、改程式或部署伺服器,請看[開發者文件](/huan/dev)。
:::
## 開始之前
你需要:
- 一組 HUAN 後台帳號。HUAN **不開放自助註冊**,帳號由管理員建立。
- 一台安裝好 HUAN 播放器的裝置(Raspberry Pi、Windows、Ubuntu 或 macOS),而且連得到你的 HUAN 伺服器。安裝方式見[安裝播放裝置](/huan/guide/install-raspberry-pi)。
## 1. 登入
打開你的 HUAN 網址,用管理員給你的 Email 或帳號登入。
登入畫面
登入後會看到總覽,上面是裝置的線上狀態與需要注意的項目。
建議第一件事是到「安全設定」[開啟兩步驟驗證](/huan/guide/two-factor)。
## 2. 上傳素材
到「素材庫」,把影片或圖片拖進上傳區。
素材庫
檔案會依序經過**上傳中 → 處理中 → 可使用**三個狀態。處理階段 HUAN 會自動轉出適合播放的版本、縮圖與預覽圖。
文字與跑馬燈不需要上傳,等一下直接寫在版面裡。
詳見[上傳素材](/huan/guide/media)。
## 3. 設計版面
到「版面」建立一份新版面,選擇畫布尺寸(一般橫式螢幕就選 1920 × 1080)。
版面編輯器:左邊素材、中間預覽、右邊屬性
HUAN 的版面是**把畫面切開**,不是自由拖放:
1. 選一個區塊,按「水平分割」把它切成左右兩半。
2. 在右側面板把第一塊的占比調成 70。
3. 從左邊的素材面板把影片拖進左半邊。
4. 選右半邊,按「放入文字」,寫上你的標語。
比例是百分比而不是像素,所以同一份版面放到不同解析度的螢幕上,構圖不會跑掉。
滿意之後按**發布**。發布會建立一個固定的版本,現場播的就是它——你之後繼續編輯草稿不會影響已經在播的畫面。
詳見[設計版面](/huan/guide/layouts)。
## 4. 配對裝置
第一次啟動播放器時,螢幕上會顯示一組 `XXXX-XXXX` 配對碼與 QR Code。
播放器啟動後顯示的配對畫面
用已經登入的瀏覽器掃描 QR Code,或到後台的「裝置 → 配對新裝置」輸入這組配對碼。確認是眼前這一台之後按下綁定,順便幫它取個名字並指定要播的預設版面。
配對碼 10 分鐘後失效,而且只能用一次。
詳見[配對裝置](/huan/guide/pairing)。
## 5. 看它動起來
綁定完成後,裝置會自己完成剩下的事:
```text
取得該播什麼
↓
下載需要的影片與圖片
↓
逐一驗證檔案完整
↓
全部就緒才切換畫面
```
在後台的「裝置」頁可以看到同步進度。目標版本與實際版本一致時,就代表現場已經在播你剛才做的畫面了。
裝置管理
## 6.(選用)安排播放時段
想讓不同時段播不同畫面的話,到「排程」建立規則:
```text
星期一至五 08:00–11:00 早餐版面
星期一至五 11:00–14:00 午餐版面
```
沒有任何排程命中的時間,裝置就播它的預設版面。
排程會下載到裝置本機,所以**就算伺服器離線,現場一樣會照時間換畫面**。
詳見[安排播放時段](/huan/guide/schedules)。
## 網路斷了會怎樣
畫面照播。
HUAN 的素材永遠是先下載到裝置上才播放,排程也存在裝置裡。網路只負責告訴裝置「內容換了」——這件事延遲幾小時甚至幾天,都不影響現在正在播的畫面。
詳見[離線播放](/huan/guide/offline)。
## 接下來
- [核心概念](/huan/guide/concepts):版面、修訂、排程與裝置之間的關係
- [管理裝置](/huan/guide/devices):改名、強制同步、重新啟動、解除綁定
- [使用者與權限](/huan/guide/users):誰可以做什麼
- [疑難排解](/huan/guide/troubleshooting):東西不對勁的時候
---
# 核心概念
理解這六個概念就理解了 HUAN。
理解這六個概念就理解了 HUAN。
## 素材(Media Asset)
一個上傳的檔案:影片、圖片或 HTML。文字與網址不需要上傳,它們直接寫在版面設定裡。
每個素材會產生數種產物:
| 產物 | 用途 | 保留 |
| ----------- | ------------------ | ------------------------- |
| `original` | 使用者上傳的原始檔 | 轉檔成功後刪除 |
| `thumbnail` | 素材庫縮圖 | 長期 |
| `preview` | 後台排版預覽 | 長期 |
| `playback` | 派送到裝置播放 | 全部 ACK 且過保留期後回收 |
詳見[素材生命週期](/huan/dev/asset-lifecycle)。
## 版面(Layout)
一份畫面的設計。包含畫布尺寸、背景、間距,以及一棵遞迴分割樹。
任何區塊都可以被水平或垂直切成兩塊,每一塊可以放文字、跑馬燈、圖片、影片、網址或上傳的 HTML。
分割用的是**比例**而不是像素,因此同一份版面在不同解析度上維持相同的構圖。詳見[版面與縮放](/huan/dev/layout-engine)。
## 修訂(Revision)
發布版面時建立的**不可變快照**。
草稿與已發布版本完全分離:你可以放心編輯草稿,現場播的還是上一次發布的內容。
裝置的目標狀態指向的是修訂 id,因此可以明確知道某台裝置在某個時刻播的是哪一版。
## 排程(Schedule)
「在哪些日子的哪個時段,哪些裝置播哪個版面」。
排程綁定 IANA 時區,正確處理日光節約時間。多筆排程同時生效時依明確定義的順序決勝:優先度 → 視窗較短 → 天數較少 → 有日期區間 → 較新 → id。
排程清單會下載到裝置本機,因此 **Server 離線時裝置仍會照時間切換版面**。詳見[排程與時區](/huan/dev/scheduling)。
## 裝置(Device)
一台已配對的播放器。
配對流程:裝置顯示 `XXXX-XXXX` 配對碼與 QR Code,已登入的管理員掃碼或輸入配對碼後確認綁定。裝置取得一組**專屬的憑證**,不同裝置不共用秘密。
裝置綁定後**不能在本機修改內容**。本機只提供裝置資訊、連線狀態與解除綁定。
## 目標狀態與回報狀態
Server 宣告「這台裝置應該是修訂 42」;裝置回報「我現在是 41」,然後自己下載、驗證、啟用,最後回報 42。
```text
Server: desiredVersion = 42
Device: reportedVersion = 41
↓ 下載
↓ 驗證 SHA-256
↓ 全部就緒才原子性切換
Device: reportedVersion = 42
```
任何一個檔案失敗,裝置就留在 41 繼續播舊內容,稍後重試。**絕不會為了下載新版本而先刪掉舊版本。**
詳見 [Desired / Reported State](/huan/dev/desired-reported-state)。
---
# 上傳素材
/app/media 是素材庫。
`/app/media` 是素材庫。
素材庫
## 可以上傳什麼
| 類型 | 格式 |
| ---- | ------------------------------ |
| 影片 | MP4、MOV、MKV、WebM、MPEG、AVI |
| 圖片 | JPEG、PNG、WebP、AVIF、GIF |
| HTML | 單一自帶資源的 `.html` |
**文字與網址不需要上傳。** 它們直接寫在版面設定裡,不佔用素材庫。
## 上傳流程
```mermaid
sequenceDiagram
participant B as 瀏覽器
participant S as Server
participant R as RustFS
participant W as Worker
B->>S: 要求上傳授權
S-->>B: 簽章網址
B->>R: 直接上傳檔案
B->>S: 通知上傳完成
S->>W: 建立轉檔工作
W->>W: ffprobe、轉檔、縮圖
W->>S: 標記為就緒
```
檔案**不經過 Server 的記憶體**,瀏覽器直接傳到物件儲存。一支 4 GB 的影片如果經過 Node.js,幾個人同時上傳就會把 Server 打掛。
## 狀態
| 狀態 | 意義 |
| ---------------- | ------------------------------- |
| **上傳中** | 檔案正在傳到物件儲存 |
| **處理中** | Worker 正在轉檔、產生縮圖與預覽 |
| **就緒** | 可以放進版面 |
| **失敗** | 轉檔失敗,畫面上會有原因 |
| **需要重新上傳** | 已經沒有可派送的副本 |
## 影片會被怎麼處理
Worker 會產生三種版本:
| 版本 | 用途 |
| ---- | ------------------------ |
| 縮圖 | 素材庫的預覽圖 |
| 預覽 | 後台排版時的低解析度播放 |
| 播放 | 實際派送到裝置的版本 |
播放版本的規格:MP4 容器、H.264、`yuv420p`、最高 1920×1080、最高 30fps、AAC 音訊、啟用 fast start。
**不做放大。** 640×360 的來源轉出來還是 640×360。長寬比一律保持原樣。
`yuv420p` 是刻意指定的:某些來源使用 `yuv444p` 或 10-bit 格式,Raspberry Pi 的硬體解碼器不支援,會退回軟體解碼而卡頓。
## 圖片會被怎麼處理
同樣產生三種版本。播放版本限制在合理尺寸——不能讓 Raspberry Pi 每次算繪都去解一張 12000×9000 的原圖。
來源含有透明資訊時會保留,不會被壓成黑底。
## HTML
第一版只支援**單一自帶資源**的 HTML 檔案。需要圖片時請用 `data:` URI 內嵌。
HTML 在裝置上以 sandbox iframe 執行,**沒有** Node、檔案系統或 Electron API 的存取權。
## 「需要重新上傳」是什麼意思
:::warning[這是產品的真實限制,不是故障]
HUAN 不永久保存原始檔,播放產物也會在所有裝置下載完成並經過保留期後回收。
:::
當下列情況同時成立時,這份素材就無法再派送:
- 原始檔已在轉檔後刪除
- 播放產物已在所有裝置 ACK 後回收
- 而你現在需要重新派送(新增了裝置、或某台裝置清掉了本機儲存)
後台會直接把它標示為「需要重新上傳」。**系統不會假裝檔案還在,然後在派送時才失敗。**
解法是重新上傳這個檔案。想降低發生機率的話,把 `DISTRIBUTION_RETENTION_HOURS` 調長(`720` 表示 30 天)。
完整理由見 [ADR-0003](/huan/dev/adr/0003-temporary-object-storage)。
## 刪除
刪除之前,HUAN 會檢查這份素材有沒有被使用:
- 草稿中的版面
- 已發布的版面修訂
- 排程
- 裝置目前的狀態
有任何一項使用中,刪除會被擋下來,並列出是哪些版面或排程在用它。請先解除引用再刪除。
**不會有無提示的刪除。**
## 轉檔失敗怎麼看
畫面上顯示的是給使用者看的說明,例如「影片轉檔失敗,來源檔案可能損毀或格式不支援」。
完整的 FFmpeg 指令、stderr 與檔案系統路徑只留在伺服器日誌裡。那些資訊對一般使用者沒有幫助,而且會洩漏伺服器的內部結構。
---
# 設計版面
/app/layouts 是版面列表,/app/layouts/:id 是編輯器。
`/app/layouts` 是版面列表,`/app/layouts/:id` 是編輯器。
版面編輯器:左側素材、中間預覽、右側屬性
## 建立版面
先決定畫布尺寸:
| 預設 | 用途 |
| ----------- | ---------------- |
| 1920 × 1080 | 一般橫式螢幕 |
| 1080 × 1920 | 直式螢幕 |
| 3840 × 2160 | 4K |
| 1280 × 720 | 較低解析度的裝置 |
也可以自訂,或選擇**使用裝置解析度**,直接從已配對裝置回報的顯示器資訊帶入。
## 遞迴分割
版面不是自由拖曳的畫布,而是一棵分割樹。任何區塊都可以被切成兩塊:
```text
根區塊(水平分割 70/30)
├─ 左 70% → 影片
└─ 右 30%(垂直分割 50/50)
├─ 上 → 標題文字
└─ 下 → 跑馬燈
```
- **水平分割**:兩塊左右並排。
- **垂直分割**:兩塊上下堆疊。
分割用的是**比例**而不是像素。因此同一份版面在 1080p 與 4K 上維持相同的構圖。
比例有 5% 到 95% 的限制——沒有這個限制,一不小心就會拖出再也點不回來的版面。
理由見 [ADR-0004](/huan/dev/adr/0004-recursive-split-layout)。
## 操作
| 操作 | 方式 |
| -------------- | ------------------------------------ |
| 選取區塊 | 點擊,或用 Tab 鍵移動焦點 |
| 水平/垂直分割 | 工具列按鈕或快捷鍵 |
| 調整比例 | 拖曳分隔線,或在右側面板輸入數值 |
| 刪除分割 | 選取後按刪除,兄弟區塊會頂上來 |
| 放入內容 | 從左側素材面板拖入,或在右側面板選擇 |
| 交換內容 | 把一個區塊拖到另一個區塊上 |
:::tip[拖曳不是唯一的方式]
每一個拖曳操作都有鍵盤或表單的替代方式。比例可以用方向鍵微調,也可以直接輸入數字。
:::
## 區塊內容
| 類型 | 設定 |
| ---------- | ------------------------------------------------------ |
| **文字** | 內容、背景色、文字色、字級、字重、水平與垂直對齊、內距 |
| **跑馬燈** | 同文字,另加方向、速度與重複間距 |
| **圖片** | 素材、填滿方式(contain/cover/fill)、背景色 |
| **影片** | 素材、填滿方式、循環、靜音、音量、背景色 |
| **網址** | https 網址,以 iframe 顯示 |
| **HTML** | 上傳的 HTML 素材,以 sandbox iframe 顯示 |
字型固定使用 sans-serif,HUAN 不提供字型上傳。
填滿方式的預設是 `contain`,也就是不變形。
### 關於 iframe
:::warning[很多網站不能被嵌入]
外部網站可以用 `X-Frame-Options` 或 `Content-Security-Policy: frame-ancestors` 明確禁止被嵌入。遇到這種網站,區塊會顯示空白。
這是對方網站的安全設定,**HUAN 不會也不應該繞過它**。改用可嵌入的來源,或把內容做成上傳的 HTML。
:::
## 全域設定
- **背景色**:畫布本身的底色,也是螢幕長寬比與畫布不同時留白區域的顏色。
- **背景圖片**:可選。
- **間距**:區塊之間的距離,單位是設計畫布的 px,會跟著畫布等比縮放。
間距是從可用空間扣掉的,所以不論分幾層,所有區塊的外緣永遠貼齊畫布邊界。
## 草稿與發布
編輯器改的是**草稿**,會自動儲存。現場播的還是上一次發布的內容,所以可以放心編輯。
按下發布會建立一筆**不可變的修訂**:
```text
版面 3
├─ 修訂 12(2026-08-01)
├─ 修訂 13(2026-08-14)
└─ 修訂 14(2026-09-02)← 目前
```
裝置的目標狀態指向修訂 id,因此可以明確知道某台裝置播的是哪一版,也可以在需要時回溯。
發布前會檢查所有引用到的素材都已經就緒。有素材還在處理中或需要重新上傳時,發布會被擋下並列出是哪幾個。
## 預覽和實機一模一樣
編輯器的預覽和裝置播放器用的是**同一套版面引擎**(`@huan/layout-engine`)。
這不是為了少寫程式碼,而是為了保證兩邊一致。如果後台自己算一套、裝置再算一套,遲早會出現「後台是 70/30,實機變成 68/32」這種沒人查得出來的偏差。
幾何計算有針對 1920×1080、1280×720、3840×2160 與 1080×1920 的單元測試,斷言的是實際數值。
## 螢幕長寬比不一樣時
裝置的螢幕和設計畫布長寬比不同時,HUAN 會把整份畫布**等比縮放並置中**,剩下的區域填入版面背景色。
**不做拉伸。** 變形的看板比黑邊難看得多,而且客戶不會知道問題出在哪裡。
---
# 安排播放時段
/app/schedules 決定「在哪些日子的哪個時段,哪些裝置播哪個版面」。
`/app/schedules` 決定「在哪些日子的哪個時段,哪些裝置播哪個版面」。
排程管理
## 一筆排程包含什麼
| 欄位 | 說明 |
| -------------- | ------------------------------------------ |
| 名稱 | 給人看的識別 |
| 版面 | 要播的版面。實際派送的是它目前已發布的修訂 |
| 時區 | IANA 時區,例如 `Asia/Taipei` |
| 優先度 | 數字愈大愈優先 |
| 日期區間 | 可留空表示不限 |
| 星期 | 星期幾生效 |
| 開始/結束時間 | `HH:MM` |
| 目標裝置 | 這筆排程套用在哪些裝置上 |
典型用法:
```text
星期一至五 08:00–11:00 早餐版面
星期一至五 11:00–14:00 午餐版面
星期一至五 14:00–17:00 下午茶版面
```
沒有任何排程命中的時段,裝置播它的**預設版面**(在裝置設定裡指定)。
## 時區
排程存的是 IANA 時區名稱,不是 UTC 偏移。
這很重要:UTC 偏移會隨日光節約時間改變,時區不會。一個設在 `America/New_York` 早上八點的排程,在三月和十一月對應到不同的 UTC 時刻。只存「UTC 13:00」的話,換季的那個週末看板就會在錯的時間換畫面。
判定完全用當地牆上時間進行,日光節約時間、半小時偏移、南半球的反向切換全都自動正確。
## 跨午夜
結束時間早於或等於開始時間就代表跨午夜。視窗**錨定在開始的那一天**:
```text
星期一 22:00–02:00
= 星期一晚上十點 → 星期二凌晨兩點
```
星期二凌晨一點會命中(屬於星期一開始的視窗),星期三凌晨一點不會。
## 衝突怎麼決勝
兩筆排程同時生效時,依序比較:
1. **優先度**較大的勝出
2. **每日視窗較短**的勝出
3. **指定天數較少**的勝出
4. **有日期區間**的勝過沒有的
5. **最後修改時間較新**的勝出
6. 以 **id** 作為最後的決勝
最後一條的存在是為了讓結果**永遠是決定性的**。兩筆條件完全相同的排程不會隨機挑一筆。
實務上的用法:
```text
優先度 100 星期一至五 08:00–18:00 一般營業版面
優先度 500 星期一至五 12:00–12:30 午間限時活動 ← 勝出
```
## 離線也會照時間切換
排程清單本身會下載到裝置本機。判定完全在裝置上進行。
**Server 離線時,裝置依然會照時間切換版面。**
裝置不會在每個時間點去問 Server「現在要播什麼」——那種設計在網路正常時很優雅,網路一斷就整個停擺,而數位看板最常出問題的地方就是網路。
## 停用
排程可以停用而不刪除。停用的排程不會參與判定,但設定都留著,隨時可以再打開。
---
# 管理裝置
/app/devices 是裝置列表,/app/devices/:id 是單一裝置的詳情。
`/app/devices` 是裝置列表,`/app/devices/:id` 是單一裝置的詳情。
裝置管理
## 看得到什麼
| 欄位 | 說明 |
| ------------------- | -------------------------------- |
| 名稱 | 可以隨時改 |
| 線上/離線 | 依 heartbeat 判定 |
| 平台與架構 | linux/win32/darwin,x64/arm64 |
| 螢幕解析度 | 裝置回報的顯示器資訊 |
| App 版本 | 播放器的版本 |
| 最後上線 | 最後一次 heartbeat 的時間 |
| 目前版面 | 現在正在播的版面修訂 |
| 目標版本 / 回報版本 | 兩者相同代表已同步 |
| 磁碟空間 | 剩餘與總容量 |
| 最後同步 | 最後一次成功套用目標狀態的時間 |
Raspberry Pi 上還會回報 CPU 溫度。Windows 與 macOS 讀不到就顯示「—」,**不會編一個數字出來**。
## 目標版本與回報版本
這兩個數字是理解裝置狀態的關鍵。
```text
目標 42 / 回報 42 → 已同步
目標 42 / 回報 41 → 正在下載新版本,目前仍播 41
```
裝置落後不一定是問題——大檔案下載本來就要時間。持續落後才需要看一下詳情頁的素材同步狀態。
常見原因:
- **磁碟空間不足**:裝置會回報儲存錯誤,後台會顯示出來
- **某個素材下載失敗**:裝置會留在舊版本並重試,這是**正確**行為
- **素材已被回收**:如果素材標示為需要重新上傳,裝置永遠拿不到它
## 可以做什麼
| 操作 | 說明 |
| -------------- | -------------------------- |
| 重新命名 | 只改顯示名稱,不影響配對 |
| 設定預設版面 | 沒有排程命中時播的版面 |
| 強制同步 | 讓裝置立刻重新取得目標狀態 |
| 重新啟動播放器 | 遠端重啟算繪行程 |
| 解除綁定 | 撤銷憑證,裝置回到配對畫面 |
所有具破壞性的操作都需要二次確認。
## 解除綁定
解除綁定會:
1. 撤銷這台裝置的憑證
2. 清除帳戶綁定
3. 讓裝置回到配對畫面
被撤銷的憑證立即失效。**不會留下任何舊帳戶仍可遠端控制的憑證。**
裝置端也可以自己解除綁定,見 [Device 解除綁定](/huan/guide/unbind)。
## 配對
裝置第一次啟動會顯示 `XXXX-XXXX` 配對碼與 QR Code。
後台的配對確認畫面
用已登入的後台掃描 QR Code,或到 `/pair` 手動輸入配對碼。確認裝置資訊後綁定,可以順便命名並指定預設版面。
配對碼是隨機產生、10 分鐘過期、只能使用一次。字母表排除了 0/O/1/I,因為這串字要被人從螢幕上抄下來。
詳見 [Device 配對](/huan/guide/pairing)。
## 稽核
裝置的配對、解除綁定、重新命名、強制同步與重新啟動都會留下稽核紀錄。
---
# 配對裝置
裝置第一次啟動時還不屬於任何帳戶。配對就是把它綁定到你的 HUAN 的過程。
裝置第一次啟動時還不屬於任何帳戶。配對就是把它綁定到你的 HUAN 的過程。
## 裝置端
啟動後畫面顯示:
```text
HUAN
讙
7K4M-9PXA
```
以及一個 QR Code。
裝置端的配對畫面
## 後台端
用已登入的瀏覽器掃描 QR Code,或直接到 `/pair` 手動輸入配對碼。
確認畫面會顯示這台裝置回報的名稱、平台、架構與 App 版本,讓你確定綁的是眼前這一台。可以順便重新命名並指定預設版面。
按下綁定之後,裝置畫面會顯示「已綁定」並進入播放器。
## 流程
```mermaid
sequenceDiagram
participant D as 裝置
participant S as Server
participant A as 管理員
D->>S: 要求配對碼
S-->>D: 7K4M-9PXA + 配對 token
D->>D: 顯示配對碼與 QR Code
loop 每隔幾秒
D->>S: 用配對 token 查詢狀態
S-->>D: 尚未配對
end
A->>S: 掃碼或輸入配對碼
S-->>A: 裝置資訊
A->>S: 確認綁定
S->>S: 建立裝置與憑證
D->>S: 查詢狀態
S-->>D: 已配對 + 一次性憑證
D->>D: 安全保存憑證,進入播放器
```
## 配對碼
- 8 個字元,顯示成 `XXXX-XXXX`
- 密碼學隨機產生,不可預測
- 10 分鐘過期
- 只能使用一次
- 字母表排除 0、O、1、I
排除易混淆字元是因為這串字要被人從牆上的螢幕抄到手機或電腦裡。
## 為什麼有兩個秘密
配對碼給人看,配對 token 給裝置用。
裝置在要求配對碼的同時會拿到一個配對 token,只有持有這個 token 的裝置能領取配對結果。
這表示**即使有人猜到或看到配對碼,也搶不走那組憑證**——他可以完成綁定,但憑證只會交給發起配對的那一台裝置。
## 裝置憑證
配對完成後,裝置取得一組**專屬的憑證**。
- 每台裝置各自獨立,不共用秘密。撤銷一台不影響其他台。
- Server 只保存憑證的 SHA-256,**憑證原值只在配對當下回傳一次**。
- 憑證在裝置上使用作業系統提供的加密能力保存(Electron 的 `safeStorage`),不以明文寫在設定檔裡。
- 憑證不會出現在任何日誌或稽核紀錄中。
## 配對碼過期了
重新啟動裝置就會產生新的一組。
## 已綁定的裝置能做什麼
**不能在裝置本機修改版面、素材或排程。** 內容一律由後台控制。
本機只提供:
- 裝置基本資訊
- 網路與連線狀態
- 配對狀態
- 解除綁定
透過系統匣、選單或鍵盤快捷鍵開啟,不影響正常播放。
---
# 離線播放
這是 HUAN 最重要的一項行為。
這是 HUAN 最重要的一項行為。
## 保證
**網路斷線時,HUAN 繼續播放目前的內容。**
不會有這種事:
```text
網路斷線
↓
黑畫面
```
一面看板的價值就是它一直亮著。客戶不會知道是網路的問題,他們只會看到自己花錢買的螢幕變成一塊黑板。
## 為什麼做得到
素材永遠**先下載到裝置本機**才播放。正常播放時裝置完全不需要網路——它讀的是自己硬碟上的檔案。
排程清單也存在本機,因此時段切換也不需要 Server。
網路只負責一件事:告訴裝置「內容變了」。這件事延遲幾分鐘、幾小時,甚至幾天,都不影響現在正在播的畫面。
理由見 [ADR-0005](/huan/dev/adr/0005-local-first-playback)。
## 更新怎麼進行
```mermaid
flowchart TD
A["目前播放版本 A"] --> B["收到通知:有版本 B"]
B --> C["背景下載版本 B 需要的全部素材"]
C --> D{"每個檔案
SHA-256 都正確?"}
D -->|否| E["刪除暫存檔並重試"]
E --> C
D -->|是| F["全部就緒"]
F --> G["原子性切換到版本 B"]
C -->|任一檔案最終失敗| H["維持播放版本 A"]
H --> I["稍後重試"]
I --> C
```
關鍵在於:**版本 A 全程都在播。**
- 下載期間播 A
- 驗證期間播 A
- 任何一個檔案失敗,就繼續播 A
- 只有全部就緒才切到 B
**絕對不會先刪除版本 A 的檔案再下載版本 B。** 舊版本的素材要等新版本成功啟用並經過保留期之後才會被回收。
## 校驗
每個檔案都用 SHA-256 驗證:
```text
下載到 .part 暫存檔(邊寫邊算雜湊)
↓
比對 manifest 裡的值
↓
符合 → rename 成正式檔名 → 回報 ACK
不符 → 刪除暫存檔 → 重試
```
雜湊不符時**絕不回報 ACK**。回報一個其實沒下載成功的檔案,會讓 Server 以為可以回收派送產物——那才是真正無法挽回的錯誤。
## 重新連線
WebSocket 斷線後以指數退避加抖動重連:
```text
1s → 2s → 4s → 8s → 16s → 30s → 60s(上限)
```
每次等待時間都加上隨機抖動。整個賣場的看板同時斷線時,沒有抖動就會在同一秒一起重連,把剛恢復的 Server 再打掛一次。
即使 WebSocket 正常,裝置仍然每 5 分鐘做一次完整狀態同步,不完全依賴推播。
## 斷電之後
所有本機狀態的寫入都是原子的:先寫暫存檔、`fsync`、再 `rename`。
看板通常裝在牆上,沒有 UPS,斷電是常態而不是意外。半寫入的 manifest 會讓裝置開機後不知道自己該播什麼——原子寫入讓這件事不可能發生。
重新開機後:
```text
載入最後一份已知良好的狀態
↓
繼續播放
↓
在背景重新連線 Server
```
**開機不需要 Server 在線。**
## 儲存空間
裝置有本機快取上限。空間不足時:
1. 先回收未被引用的舊素材
2. 仍然不足就向 Server 回報儲存錯誤,後台會顯示
回收**永遠不會刪除**:
- 目前啟用版本的素材
- 下一個排程版本需要的素材
- 正在下載中的檔案
## 下載節流
裝置同時最多下載 2 到 3 個檔案,不會一次抓五十支影片把現場網路塞死。
已經存在且雜湊正確的檔案會直接跳過,重新開機不會重下載一輪。伺服器支援 `Range` 時可以續傳。
簽章網址過期時裝置會重新索取,**不會讓整次同步失敗**。
---
# 解除綁定
解除綁定會切斷裝置與帳戶的關係,讓它回到配對畫面。
解除綁定會切斷裝置與帳戶的關係,讓它回到配對畫面。
## 兩種方式
**從後台**:`/app/devices/:id` → 解除綁定。需要二次確認。
**從裝置本機**:開啟裝置資訊面板 → 解除綁定。同樣需要二次確認。
兩種方式的結果相同。
## 會發生什麼
```text
撤銷裝置憑證
↓
清除帳戶綁定與目標狀態
↓
回到配對畫面
```
被撤銷的憑證**立即失效**。裝置下一次請求就會被拒絕。
:::tip[憑證立即失效]
解除綁定之後,不會留下任何舊帳戶仍可遠端控制這台裝置的憑證。
:::
## 離線時解除綁定
在裝置本機解除綁定時,如果剛好連不上 Server:
1. **本機立即停止接受帳戶控制。** 不等 Server 確認。
2. 本機保留一個「待撤銷」狀態。
3. 下次連上線時完成 Server 端的撤銷。
待撤銷狀態**不會因為收到過期的伺服器回應而被還原**。已經解除的綁定不會自己恢復。
這一點很重要:使用者按下解除綁定的當下就期待它生效了。如果因為網路不通而讓裝置繼續接受遠端控制,那個按鈕就是騙人的。
## 重新配對
解除綁定後裝置回到配對畫面,顯示一組**新的**配對碼。
舊的憑證已經失效,重新綁定會產生全新的裝置紀錄。
## 本機素材
解除綁定後,裝置本機的素材檔案會在下一次垃圾回收時清除。
需要立刻清空的話,重新安裝應用程式或直接刪除資料目錄。
## 稽核
不論從哪一端發起,解除綁定都會留下稽核紀錄,包含操作者、裝置與時間。
---
# 在 Raspberry Pi 上安裝
Raspberry Pi 是 HUAN 的主要目標裝置,但產品沒有綁死在它上面。同一份程式碼也跑在 Ubuntu、Windows 與 macOS 上。
Raspberry Pi 是 HUAN 的主要目標裝置,但**產品沒有綁死在它上面**。同一份程式碼也跑在 Ubuntu、Windows 與 macOS 上。
## 建議規格
| 項目 | 需求 |
| -------- | --------------------------------------- |
| 機型 | Raspberry Pi 4 以上 |
| 作業系統 | Raspberry Pi OS 64-bit(Bookworm 以上) |
| 記憶體 | 最低 2GB,**建議 4GB** |
| 儲存 | 32GB 以上的 A2 等級 microSD,或 USB SSD |
| 網路 | 有線網路優先 |
:::tip[用 SSD 而不是 SD 卡]
microSD 卡在持續寫入下的壽命有限,而 HUAN 會在同步時寫入不少資料。長期部署建議從 USB SSD 開機。
:::
## 安裝
從 [Releases](https://github.com/linyao-tw/huan/releases) 下載 **linux-arm64** 的版本。
AppImage:
```sh
chmod +x HUAN-Device-*-linux-arm64.AppImage
./HUAN-Device-*-linux-arm64.AppImage
```
Debian 套件:
```sh
sudo apt install ./HUAN-Device-*-linux-arm64.deb
huan-device
```
## 開機自動啟動
Raspberry Pi OS 使用 systemd,但 **HUAN 不假設系統上一定有 systemd**——它只是最常見的情況。
```ini
# ~/.config/systemd/user/huan-device.service
[Unit]
Description=HUAN Device Player
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/huan-device
Restart=always
RestartSec=10
[Install]
WantedBy=graphical-session.target
```
```sh
systemctl --user enable --now huan-device
loginctl enable-linger "$USER" # 讓服務在沒有登入時也能執行
```
## 螢幕設定
### 關閉螢幕保護與休眠
```sh
sudo apt install xscreensaver
xset s off
xset -dpms
xset s noblank
```
或在 `~/.config/wayfire.ini` 加上:
```ini
[idle]
dpms_timeout = -1
screensaver_timeout = -1
```
### 直式螢幕
```sh
# Wayland(Raspberry Pi OS Bookworm 預設)
wlr-randr --output HDMI-A-1 --transform 90
```
版面的畫布尺寸要跟著改成 1080×1920。裝置回報的顯示器資訊會反映實際的方向,建立版面時可以直接選「使用裝置解析度」。
## 影片播放
Worker 產生的播放檔已經針對 Raspberry Pi 最佳化:
- H.264(不是 H.265)
- `yuv420p` 像素格式
- 最高 1920×1080
- 最高 30fps
`yuv420p` 是刻意指定的:某些來源使用 `yuv444p` 或 10-bit 格式,Pi 的硬體解碼器不支援,會退回軟體解碼而卡頓。
### 增加 GPU 記憶體
`/boot/firmware/config.txt`:
```ini
gpu_mem=256
```
## 溫度
Raspberry Pi 上讀得到 CPU 溫度,會在 heartbeat 中回報,後台可以看到。
Windows 與 macOS 上讀不到,回報 `null`,後台顯示「—」。**不會為了統一型別而編一個數字。**
裝置超過 80°C 會開始降頻,畫面可能變卡。裝在密閉機殼裡的 Pi 建議加散熱片或風扇。
## 沒有的假設
HUAN **不會假設**系統上一定存在:
- `apt`
- `vcgencmd`
- `systemd`
這些平台差異都封裝在 `PlatformAdapter` 介面裡。讀不到的資訊回報 `null`,不會讓播放器崩潰。
## 疑難排解
**畫面全黑但程式在跑**:檢查 HDMI 是不是被偵測成未連接。Pi 在開機時沒接螢幕會停用輸出。
**影片卡頓**:確認素材真的是 H.264 / `yuv420p`(後台的素材詳情看得到),並增加 GPU 記憶體。
**同步很慢**:Wi-Fi 在多數場地都不穩定,有線網路差很多。
**磁碟空間不足**:後台會顯示裝置回報的儲存錯誤。清掉不需要的排程與版面可以讓裝置回收對應的素材。
---
# 在 Ubuntu 上安裝
支援 x64 與 arm64。
支援 x64 與 arm64。
## 需求
| 項目 | 需求 |
| -------- | --------------------- |
| 版本 | Ubuntu 22.04 LTS 以上 |
| 架構 | x64 或 arm64 |
| 記憶體 | 最低 2GB,建議 4GB |
| 桌面環境 | 需要 X11 或 Wayland |
## 安裝
從 [Releases](https://github.com/linyao-tw/huan/releases) 下載對應架構的版本。
AppImage:
```sh
chmod +x HUAN-Device-*-linux-x64.AppImage
./HUAN-Device-*-linux-x64.AppImage
```
Debian 套件:
```sh
sudo apt install ./HUAN-Device-*-linux-x64.deb
huan-device
```
## 開機自動啟動
```ini
# ~/.config/systemd/user/huan-device.service
[Unit]
Description=HUAN Device Player
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/huan-device
Restart=always
RestartSec=10
[Install]
WantedBy=graphical-session.target
```
```sh
systemctl --user enable --now huan-device
loginctl enable-linger "$USER"
```
也可以用桌面環境自己的自動啟動機制,放一個 `.desktop` 檔到 `~/.config/autostart/`。
## 關閉螢幕休眠
GNOME:
```sh
gsettings set org.gnome.desktop.session idle-delay 0
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing
```
X11 通用:
```sh
xset s off -dpms
```
## 無桌面環境的伺服器版
Electron 需要一個顯示伺服器。最小安裝:
```sh
sudo apt install --no-install-recommends xserver-xorg xinit openbox
```
然後用 `~/.xinitrc` 啟動:
```sh
#!/bin/sh
exec openbox-session &
exec huan-device
```
## 憑證儲存
裝置憑證使用 Electron 的 `safeStorage`,在 Linux 上會透過 `libsecret` 存進系統的金鑰環(GNOME Keyring 或 KWallet)。
沒有可用的金鑰環服務時,`safeStorage` 會退回較弱的保護。無人值守的看板機器建議確認金鑰環在開機時會自動解鎖,或接受這個限制——裝置憑證的權限範圍僅限這一台裝置,而且隨時可以從後台撤銷。
## 疑難排解
**AppImage 無法執行**:需要 FUSE。
```sh
sudo apt install libfuse2
```
**沙箱錯誤**:某些容器或受限環境需要調整 user namespace 設定。**不要用 `--no-sandbox` 繞過**——那會關掉 Electron 的核心安全機制。
---
# 在 Windows 上安裝
支援 x64 與 arm64。
支援 x64 與 arm64。
## 需求
| 項目 | 需求 |
| ------ | -------------------------------- |
| 版本 | Windows 10 1809 以上、Windows 11 |
| 架構 | x64 或 arm64 |
| 記憶體 | 最低 4GB |
## 安裝
從 [Releases](https://github.com/linyao-tw/huan/releases) 下載對應架構的安裝檔並執行。
:::tip[未簽章的安裝檔]
安裝檔目前沒有經過程式碼簽章,Windows SmartScreen 會出現警告。選擇「其他資訊」→「仍要執行」。
要消除這個警告需要 EV 程式碼簽章憑證,可以在 CI 的 secrets 裡設定後啟用。
:::
## 開機自動啟動
**工作排程器**(建議):
1. 開啟工作排程器 → 建立工作
2. 觸發程序:登入時
3. 動作:啟動程式 → HUAN Device 的執行檔路徑
4. 設定 → 勾選「如果工作失敗,每隔一分鐘重新啟動」
**啟動資料夾**(較簡單):
按 `Win + R`,輸入 `shell:startup`,把 HUAN Device 的捷徑放進去。
## 關閉螢幕休眠
```powershell
powercfg /change monitor-timeout-ac 0
powercfg /change standby-timeout-ac 0
```
## Kiosk 模式
Windows 的指派存取(Assigned Access)可以把裝置鎖在單一應用程式上:
**設定** → **帳戶** → **其他使用者** → **設定 Kiosk**
## 溫度
Windows 上無法透過一般 API 取得 CPU 溫度,裝置回報 `null`,後台顯示「—」。
這是刻意的:**不會為了讓所有平台的欄位都有值而編一個數字出來。**
## 憑證儲存
裝置憑證使用 Electron 的 `safeStorage`,在 Windows 上由 DPAPI 加密,綁定到目前的使用者帳戶。
## ARM64
Windows on ARM 的支援取決於 Electron 對該版本的支援狀況。打包 workflow 會嘗試建置 arm64 版本;建置失敗時 x64 版本可以透過模擬執行,但效能較差。
## 疑難排解
**播放器啟動後閃退**:檢查顯示卡驅動程式。舊的驅動程式可能讓 Chromium 的 GPU 行程崩潰。
**影片播放卡頓**:確認硬體加速已啟用,並更新顯示卡驅動程式。
**連不上 Server**:檢查 Windows 防火牆有沒有擋下對外連線,以及公司網路的 Proxy 設定。
---
# 在 macOS 上安裝
支援 Intel(x64)與 Apple Silicon(arm64)。
支援 Intel(x64)與 Apple Silicon(arm64)。
## 需求
| 項目 | 需求 |
| ------ | ---------------------- |
| 版本 | macOS 12 Monterey 以上 |
| 架構 | x64 或 arm64 |
| 記憶體 | 最低 4GB |
## 安裝
從 [Releases](https://github.com/linyao-tw/huan/releases) 下載對應架構的 `.dmg`,拖進「應用程式」。
:::tip[未經公證的應用程式]
目前的版本沒有經過 Apple 公證,第一次開啟時 Gatekeeper 會擋下來。
在 Finder 裡對應用程式按右鍵 → 開啟,然後在對話框中確認。或者:
```sh
xattr -dr com.apple.quarantine /Applications/HUAN\ Device.app
```
要消除這個步驟需要 Apple Developer 帳號與公證流程,可以在 CI 的 secrets 裡設定後啟用。
:::
## 開機自動啟動
**系統設定** → **一般** → **登入項目** → 加入 HUAN Device。
或用 launchd:
```xml
Label
tw.linyao.huan.device
ProgramArguments
/Applications/HUAN Device.app/Contents/MacOS/HUAN Device
RunAtLoad
KeepAlive
```
```sh
launchctl load ~/Library/LaunchAgents/tw.linyao.huan.device.plist
```
## 關閉螢幕休眠
```sh
sudo pmset -a displaysleep 0 sleep 0
```
或用 `caffeinate` 包住播放器。
## 溫度
macOS 上沒有不需要特殊權限就能取得 CPU 溫度的公開 API,裝置回報 `null`,後台顯示「—」。
## 憑證儲存
裝置憑證使用 Electron 的 `safeStorage`,在 macOS 上存進系統的鑰匙圈。
## 適合的情境
macOS 主要用於**開發與測試**——它讓開發者不需要準備 Raspberry Pi 就能執行真正的播放器。
正式部署的看板通常會選 Raspberry Pi 或小型 Linux 電腦,成本與功耗都低得多。Mac mini 在需要 4K、色彩準確度或已經有 Apple 生態系統的場合仍然是合理的選擇。
## 疑難排解
**應用程式無法開啟,顯示「已損毀」**:這通常是 quarantine 屬性造成的,用上面的 `xattr` 指令處理。
**全螢幕時仍然看得到選單列**:在系統設定裡把「自動隱藏並顯示選單列」設為「一律」。
---
# 登入
HUAN 沒有公開註冊。帳號一律由最高權限管理員在後台建立,第一個管理員則由 CLI 建立。
HUAN **沒有公開註冊**。帳號一律由最高權限管理員在後台建立,第一個管理員則由 CLI 建立。
登入畫面
## 登入
在 `/login` 輸入 Email 或帳號,加上密碼。
啟用了兩步驟驗證的帳號會進入第二階段,輸入驗證器產生的六位數驗證碼,或改用復原碼。
## 登入保護
連續失敗會被暫時擋下。節流以**帳號**與**來源 IP** 兩個維度計算,預設是五分鐘內十次失敗。
紀錄寫在資料庫而不是記憶體,因此重啟 Server 不會把計數歸零。
## Session
登入狀態存在 `HttpOnly` cookie 裡。這表示 JavaScript 讀不到它,也就無法透過 XSS 竊取。
正式環境的 cookie 帶有 `Secure`,只會在 HTTPS 下送出。
Session 預設 14 天過期。可以在[安全設定](/huan/guide/two-factor#工作階段)看到所有登入中的裝置並個別撤銷。
## 建立第一個管理員
```sh
pnpm --filter @huan/server admin:create
```
互動式地詢問 Email、帳號、顯示名稱與密碼。密碼至少 12 個字元。
Docker 環境:
```sh
docker compose -f docker/compose.yaml exec \
-e HUAN_ADMIN_PASSWORD='<一組強密碼>' \
server node apps/server/dist/cli/create-admin.js \
--email you@example.com --username admin --display-name 管理員
```
:::danger[原始碼裡沒有預設密碼]
HUAN 不內建任何預設管理員帳號或密碼。第一個帳號一定要用這個指令建立。
:::
## 忘記密碼
第一版沒有自助的密碼重設流程(那需要可靠的郵件寄送,而自架環境往往沒有)。
請最高權限管理員到[使用者管理](/huan/guide/users)重設。所有管理員都無法登入時,用 CLI 再建立一個。
---
# 兩步驟驗證
每位使用者都可以在 /app/security 為自己的帳號啟用 TOTP 兩步驟驗證。
每位使用者都可以在 `/app/security` 為自己的帳號啟用 TOTP 兩步驟驗證。
安全設定:兩步驟驗證與工作階段
## 啟用
```mermaid
flowchart LR
A["確認目前密碼"] --> B["產生密鑰"]
B --> C["顯示 QR Code"]
C --> D["用驗證器掃描"]
D --> E["輸入驗證碼確認"]
E --> F["啟用完成
顯示復原碼"]
```
1. 進入 `/app/security`,按下啟用。
2. **輸入目前的密碼。** 這一步是為了避免有人趁你離開座位時把自己的驗證器綁上去。
3. 用驗證器 App 掃描 QR Code,或手動輸入顯示的密鑰。
4. 輸入驗證器產生的六位數驗證碼確認。
5. 記下畫面顯示的復原碼。
支援任何符合 RFC 6238 的驗證器:Google Authenticator、1Password、Bitwarden、Aegis、Authy 等等。
## 復原碼
啟用時會產生 10 組復原碼。
:::warning[只會顯示一次]
離開這個畫面之後就再也看不到了。Server 只保存雜湊,任何人都無法還原。
:::
請把它們存在密碼管理器或列印下來收好。每一組只能使用一次。
用完或遺失時可以重新產生一批——舊的會全部失效。
## 登入流程
啟用之後:
```text
輸入帳號密碼
↓
沒有 2FA → 直接登入
↓
有 2FA → 輸入驗證碼 → 登入
```
第二階段使用的是短時效、一次性的中繼憑證,**不能存取任何 API**,只能用來完成驗證。
## 時鐘誤差
驗證會容忍前後各一個時間步長,也就是大約 ±30 秒的裝置時鐘誤差。
驗證碼一直被拒絕時,先檢查手機的時間是不是設成自動同步。
同一組驗證碼不能用第二次——Server 記錄了上次成功的時間步長。
## 停用
需要同時提供目前的密碼與一組有效的驗證碼。
## 工作階段
安全設定頁面列出所有登入中的工作階段:建立時間、最後活動時間、來源 IP 與瀏覽器,目前這一個會被標示出來。
可以個別撤銷。撤銷是立即生效的——被撤銷的工作階段下一次請求就會被拒絕。
## 一件不會發生的事
TOTP 密鑰只在啟用流程回傳一次。之後它不會出現在任何 API 回應、任何日誌、任何稽核紀錄裡。
---
# 使用者與權限
/app/users 只有 superadmin 可以使用。
`/app/users` 只有 `super_admin` 可以使用。
使用者管理
## 角色
| 角色 | 權限 |
| ------------- | ---------------------------------------------------------------- |
| `super_admin` | 建立、停用、修改使用者、重設密碼、管理所有裝置、查看稽核紀錄 |
| `user` | 操作素材、版面、排程與裝置,**不能**管理其他使用者或查看稽核紀錄 |
一般使用者連 `/app/users` 這個頁面都進不去,看到的是明確的權限不足說明,而不是一片空白或被靜默重導。
## 建立使用者
需要 Email、帳號、顯示名稱、初始密碼與角色。
- 帳號只能使用小寫英數字與 `.`、`_`、`-`,長度 3 到 32。
- 密碼至少 12 個字元。這是唯一的複雜度要求——強制混合大小寫與符號只會讓人選出更容易預測的密碼。
密碼以 Argon2id 雜湊儲存,系統中沒有任何地方保存明文。
## 停用
停用的帳號無法登入,但所有紀錄都保留下來。稽核日誌裡「誰在什麼時候做了什麼」仍然指得到這個人。
**沒有硬刪除。** 刪掉使用者會讓稽核紀錄失去意義。
## 重設密碼
`super_admin` 可以直接為其他使用者設定新密碼。新密碼**只顯示一次**。
重設密碼會撤銷該使用者所有的工作階段。
## 稽核
所有使用者管理的操作都會留下稽核紀錄:建立、修改、停用、重設密碼。
紀錄包含操作者、動作、目標、時間與來源 IP,**不包含**密碼或任何憑證。
---
# 疑難排解
轉檔在伺服器端進行,一般幾十秒內完成。長時間停住通常代表轉檔程序沒有在跑,請聯絡架設 HUAN 的人。
## 素材
### 上傳卡在「處理中」
轉檔在伺服器端進行,一般幾十秒內完成。長時間停住通常代表轉檔程序沒有在跑,請聯絡架設 HUAN 的人。
### 素材顯示「轉檔失敗」
常見原因是檔案損毀,或格式其實不是副檔名寫的那一種。畫面上會有一句說明。重新輸出成 MP4(H.264)再上傳通常就好了。
### 素材顯示「需要重新上傳」
:::warning[這不是故障,是產品的設計]
HUAN **不永久保存**你上傳的原始檔。
:::
原始檔在轉檔完成後就會刪除,派送用的播放檔也會在所有裝置都拿到之後回收,因為正式的播放副本已經存在裝置本機了。
當下列情況同時成立,這份素材就沒有可以派送的副本了:
- 原始檔已刪除
- 播放檔已回收
- 而你現在需要重新派送(新增了一台裝置,或某台裝置清掉了本機資料)
**解法是重新上傳這個檔案。** 後台會直接標示出來,不會等到派送時才失敗。
想降低發生機率的話,請架設者把保留期調長。詳見[素材生命週期](/huan/dev/asset-lifecycle)。
### 刪不掉素材
素材正在被草稿、已發布的版面、排程或某台裝置使用時,刪除會被擋下來,畫面上會列出是哪些地方在用它。請先把那些地方換掉或移除,再回來刪。
## 版面
### 網頁區塊一片空白
很多網站會明確禁止被嵌入(`X-Frame-Options` 或 `frame-ancestors`)。這是對方網站的安全設定,HUAN 不會也不應該繞過它。
改用允許嵌入的來源,或把內容做成一個 HTML 檔上傳。
### 上傳的 HTML 沒有作用
目前只支援**單一自帶資源**的 HTML 檔案。外部的 `