mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(docs-i18n): translate all documentation into 20 languages
All 181 docs markdown files translated into 20 languages (apps/docs/<locale>/**). Companion to the i18n code PR; admin-merged because the file count exceeds GitHub's per-PR CI trigger limit. Validated by pnpm i18n:check (all surfaces, 0 stale/missing) and a clean all-locale docs build.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
---
|
||||
description: "SnapOtter 的 monorepo 結構、app 與套件架構、請求生命週期,以及資源占用。"
|
||||
i18n_source_hash: 9e8f80499a37
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 733f35af8cb1
|
||||
---
|
||||
|
||||
# 架構 {#architecture}
|
||||
|
||||
SnapOtter 是一個以 pnpm workspaces 與 Turborepo 管理的 monorepo。它以 3 容器的 Docker Compose 堆疊部署:SnapOtter app 映像檔、PostgreSQL 17 與 Redis 8。
|
||||
|
||||
## 專案結構 {#project-structure}
|
||||
|
||||
```
|
||||
snapotter/
|
||||
├── apps/
|
||||
│ ├── api/ # Fastify backend
|
||||
│ ├── web/ # React + Vite frontend
|
||||
│ └── docs/ # This VitePress site
|
||||
├── packages/
|
||||
│ ├── image-engine/ # Sharp-based image operations
|
||||
│ ├── media-engine/ # FFmpeg spawn + progress parsing
|
||||
│ ├── doc-engine/ # qpdf, LibreOffice, ghostscript wrappers
|
||||
│ ├── ai/ # Python AI model bridge
|
||||
│ └── shared/ # Types, constants, i18n
|
||||
└── docker/ # Dockerfile and Compose config
|
||||
```
|
||||
|
||||
## 套件 {#packages}
|
||||
|
||||
### `@snapotter/image-engine` {#snapotter-image-engine}
|
||||
|
||||
以 [Sharp](https://sharp.pixelplumbing.com/) 為基礎建置的核心影像處理程式庫。它處理所有非 AI 的操作:resize、crop、rotate、flip、convert、compress、strip metadata,以及色彩調整(亮度、對比、飽和度、灰階、懷舊、反轉、色版)。
|
||||
|
||||
此套件沒有網路相依,完全在程序內執行。
|
||||
|
||||
### `@snapotter/ai` {#snapotter-ai}
|
||||
|
||||
一個橋接層,呼叫 Python 指令碼進行 ML 操作。首次使用時,橋接層會啟動一個常駐的 Python 分派器程序,預先匯入沉重的程式庫(PIL、NumPy、MediaPipe、rembg),使後續的 AI 呼叫可略過匯入負擔。若分派器尚未就緒,橋接層會退回為每個請求產生一個全新的 Python 子程序。
|
||||
|
||||
**模型不會預先載入。** 每個工具指令碼會在請求時從磁碟載入其模型權重,並在請求結束時捨棄。完整的記憶體剖析請參閱[資源占用](#resource-footprint)。
|
||||
|
||||
支援的操作:背景移除(rembg/BiRefNet)、放大(RealESRGAN)、臉部模糊(MediaPipe)、臉部強化(GFPGAN/CodeFormer)、物件擦除(LaMa ONNX)、OCR(PaddleOCR/Tesseract)、上色(DDColor)、噪點移除、紅眼移除、相片修復、護照相片產生、透明度修正(BiRefNet HR-matting),以及內容感知縮放(Go caire 二進位檔)。
|
||||
|
||||
Python 指令碼位於 `packages/ai/python/`。Docker 映像檔會在建置期間預先下載所有模型權重,因此容器可完全離線運作。
|
||||
|
||||
### `@snapotter/shared` {#snapotter-shared}
|
||||
|
||||
前端與後端共用的 TypeScript 型別、常數(例如 `APP_VERSION` 與工具定義)以及 i18n 翻譯字串。
|
||||
|
||||
## 應用程式 {#applications}
|
||||
|
||||
### API(`apps/api`) {#api-apps-api}
|
||||
|
||||
一個 Fastify v5 伺服器,公開橫跨五種模態(image、video、audio、PDF、file)的 241 個工具路由,負責處理:
|
||||
- 檔案上傳、暫存工作區管理,以及持久化檔案儲存
|
||||
- 具版本鏈的使用者檔案資料庫(`user_files` 資料表)- 每個處理結果都連回其來源檔案並記錄套用了哪個工具,並為 Files 頁面自動產生縮圖
|
||||
- 工具執行(將每個工具請求路由至影像引擎或 AI 橋接層)
|
||||
- 管線協調(依序串接多個工具)
|
||||
- 透過 BullMQ 工作佇列(pools:image、media、ai、docs、system)進行具並行控制的批次處理
|
||||
- 使用者驗證、RBAC(admin/user 角色與完整權限集)、API 金鑰管理,以及速率限制
|
||||
- 團隊管理 - 僅限 admin 的 CRUD;使用者透過其個人檔案上的 `team` 欄位指派至團隊
|
||||
- 執行階段設定 - `settings` 資料表中的鍵值儲存,可控制 `disabledTools`、`enableExperimentalTools`、`loginAttemptLimit` 與其他運維旋鈕,無需重新部署
|
||||
- 透過資料庫支援的設定進行自訂品牌與執行階段偏好設定
|
||||
- 位於 `/api/docs` 的 Scalar/OpenAPI 文件
|
||||
- 在正式環境中以 SPA 形式提供已建置的前端
|
||||
|
||||
主要相依套件:Fastify、Drizzle ORM(pg-core、node-postgres)、Sharp、BullMQ、ioredis、以及用於驗證的 Zod。
|
||||
|
||||
伺服器會在 SIGTERM/SIGINT 時處理平順關機:排空 HTTP 連線、停止 BullMQ workers、關閉 Python 分派器,並關閉資料庫連線。
|
||||
|
||||
### Web(`apps/web`) {#web-apps-web}
|
||||
|
||||
一個以 Vite 建置的 React 19 單頁應用程式。使用 Zustand 進行狀態管理、Tailwind CSS v4 進行樣式設計、Lucide 提供圖示。透過 REST 與 SSE(用於進度追蹤)與 API 溝通。
|
||||
|
||||
頁面包含工具工作區、用於管理持久化上傳與結果的 Files 頁面、自動化/管線建構器,以及管理員設定面板。
|
||||
|
||||
已建置的前端在正式環境中由 Fastify 後端提供,因此 Docker 容器中沒有獨立的 web 伺服器。
|
||||
|
||||
### Docs(`apps/docs`) {#docs-apps-docs}
|
||||
|
||||
即此 VitePress 網站。在推送至 `main` 時自動部署至 Cloudflare Pages。
|
||||
|
||||
## 請求如何流動 {#how-a-request-flows}
|
||||
|
||||
1. 使用者在 web UI 中選取工具並上傳檔案。
|
||||
2. 前端將含檔案與設定的 multipart POST 送往 `/api/v1/tools/:section/:toolId`。
|
||||
3. API 路由以 Zod 驗證輸入,然後分派處理。
|
||||
4. 對於標準工具,工作會依模態排入適當的 BullMQ pool(image、media 或 docs)。程序內的 BullMQ worker 會根據 EXIF 中繼資料自動校正影像方向、執行工具的處理函式,並回傳結果。
|
||||
5. 對於 AI 工具,TypeScript 橋接層會將請求送往常駐的 Python 分派器(或退回產生一個全新的子程序),等待其完成,並讀取輸出檔案。
|
||||
6. 工作進度會持久化至 PostgreSQL 中的 `jobs` 資料表,因此狀態可在容器重新啟動後保留。即時更新透過位於 `/api/v1/jobs/:jobId/progress` 的 SSE 傳遞。
|
||||
7. API 回傳一個 `jobId` 與 `downloadUrl`。使用者從 `/api/v1/download/:jobId/:filename` 下載處理後的檔案。
|
||||
|
||||
對於管線,API 會將每個步驟的輸出作為下一步的輸入,依序執行。
|
||||
|
||||
對於批次處理,API 會使用具逐步驟子工作的 BullMQ flows,並回傳一個含所有處理後檔案的 ZIP 檔案。
|
||||
|
||||
## 資源占用 {#resource-footprint}
|
||||
|
||||
SnapOtter 的設計以低閒置記憶體使用為目標。啟動時不會預先載入或保持任何暖機狀態。
|
||||
|
||||
### 閒置時 {#at-idle}
|
||||
|
||||
Node.js/Fastify 程序、PostgreSQL 與 Redis 都在執行。三個容器(Node.js 程序、Postgres 與 Redis)的典型閒置 RAM 為 **約 200-300 MB**。沒有 Python 程序,記憶體中也沒有模型權重。
|
||||
|
||||
### 什麼會啟動,以及何時啟動 {#what-starts-and-when}
|
||||
|
||||
| 元件 | 啟動時機 | 作用時的記憶體 |
|
||||
|-----------|-------------|---------------------|
|
||||
| Fastify 伺服器 + Postgres + Redis | 容器啟動 | 合計約 200-300 MB |
|
||||
| BullMQ workers | 容器啟動(程序內) | 每個 pool 一個 worker(image、media、ai、docs、system) |
|
||||
| Python 分派器 | 首次 AI 工具請求 | Python 直譯器 + 預先匯入的程式庫(PIL、NumPy、MediaPipe、rembg)- 無模型權重 |
|
||||
| AI 模型權重 | 特定工具的請求期間 | 從磁碟載入,請求結束時釋放 |
|
||||
|
||||
### 模型載入 {#model-loading}
|
||||
|
||||
所有模型權重檔案(合計數 GB)始終位於 `/opt/models/` 的磁碟上。每個 AI 工具指令碼僅在請求期間將自己的模型載入記憶體,之後即釋放。有些指令碼會在推論後明確呼叫 `del model` 與 `torch.cuda.empty_cache()`,以確保記憶體立即歸還。
|
||||
|
||||
請求之間沒有模型快取。連續執行同一個 AI 工具時,每次都會重新載入模型。這使得閒置記憶體趨近於零,代價是每個 AI 請求都會有模型載入延遲。
|
||||
|
||||
### 首次 AI 請求的冷啟動 {#first-ai-request-cold-start}
|
||||
|
||||
容器啟動時 Python 分派器並未執行。首次 AI 請求會並行觸發兩件事:分派器開始在背景暖機,而請求本身則退回為一次性的 Python 子程序產生。一旦分派器發出就緒信號,所有後續 AI 請求都會直接使用它,並略過子程序產生的成本。
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
description: "所有 SnapOtter 環境變數及其預設值。設定驗證、儲存、AI 模型、分析等。"
|
||||
i18n_source_hash: 8e9e9ca2840c
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: e2ec72107c68
|
||||
---
|
||||
|
||||
# 設定 {#configuration}
|
||||
|
||||
所有設定皆透過環境變數完成。每個變數都有合理的預設值,因此 SnapOtter 無需設定任何變數即可開箱即用。
|
||||
|
||||
## 環境變數 {#environment-variables}
|
||||
|
||||
### 伺服器 {#server}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `PORT` | `1349` | 伺服器監聽的連接埠。 |
|
||||
| `RATE_LIMIT_PER_MIN` | `1000` | 每個 IP 每分鐘的最大請求數。設為 0 可停用速率限制。 |
|
||||
| `CORS_ORIGIN` | (空) | CORS 允許來源的逗號分隔清單,留空則僅限同源。 |
|
||||
| `LOG_LEVEL` | `info` | 日誌詳細程度。可為下列其中之一:`fatal`、`error`、`warn`、`info`、`debug`、`trace`。 |
|
||||
| `TRUST_PROXY` | `true` | 信任來自反向代理的 `X-Forwarded-For` 標頭。若不在代理後方,請設為 `false`。 |
|
||||
|
||||
### 驗證 {#authentication}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `AUTH_ENABLED` | `false` | 設為 `true` 以要求登入。Docker 映像檔預設為 `true`。 |
|
||||
| `DEFAULT_USERNAME` | `admin` | 初始 admin 帳號的使用者名稱。僅於首次執行時使用。 |
|
||||
| `DEFAULT_PASSWORD` | `admin` | 初始 admin 帳號的密碼。請於首次登入後變更。 |
|
||||
| `MAX_USERS` | `0`(無上限) | 註冊使用者帳號的最大數量。設為 0 為無上限。 |
|
||||
| `SESSION_DURATION_HOURS` | `168` | 登入工作階段的存活時間(小時)(預設為 7 天)。 |
|
||||
| `SKIP_MUST_CHANGE_PASSWORD` | - | 設為任何非空值即可略過首次登入時強制變更密碼的提示 |
|
||||
|
||||
### 儲存 {#storage}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `STORAGE_MODE` | `local` | `local` 或 `s3`。S3/MinIO 需要具備 s3_storage 功能的授權。 |
|
||||
| `DATABASE_URL` | `postgres://snapotter:snapotter@postgres:5432/snapotter` | PostgreSQL 連線字串。 |
|
||||
| `REDIS_URL` | `redis://redis:6379` | Redis 連線字串(用於 BullMQ 工作佇列)。 |
|
||||
| `WORKSPACE_PATH` | `./tmp/workspace` | 處理期間暫存檔案的目錄。會自動清理。 |
|
||||
| `FILES_STORAGE_PATH` | `./data/files` | 持久化使用者檔案(已上傳影像、已儲存結果)的目錄。 |
|
||||
|
||||
### 內嵌模式 {#embedded-mode}
|
||||
|
||||
以未設定 `DATABASE_URL` 與 `REDIS_URL` 的方式執行映像檔,它會在容器內啟動自己的 PostgreSQL 17 與 Redis,繫結至 loopback,所有資料位於 `/data` 磁碟區。這重現了單一指令的 `docker run` 體驗,適用於快速上手、homelab 與從 1.x 升級。這是一條便利路徑,而非正式環境部署:正式環境請執行具獨立 PostgreSQL 與 Redis 的 3 容器 Compose 堆疊。內嵌模式需要以 root 執行容器,且與任意 UID 的執行環境(OpenShift、Kubernetes `runAsNonRoot`)不相容;在那些環境請使用 Compose。
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `EMBEDDED` | `auto` | 當 `DATABASE_URL` 與 `REDIS_URL` 皆未設定時自動啟用。設為 `0` 可停用它(此時若未設定外部 `DATABASE_URL`/`REDIS_URL`,app 會快速失敗,而非默默啟動容器內資料庫)。 |
|
||||
| `REDIS_MAXMEMORY` | `512mb` | 內嵌 Redis 的記憶體上限(僅限內嵌模式)。在記憶體受限的主機(例如 Raspberry Pi)上請調低它。 |
|
||||
|
||||
從 1.x 升級:將你的舊 `snapotter.db` 放在磁碟區中的 `/data/snapotter.db`,內嵌模式會在首次啟動時將它匯入內嵌的 PostgreSQL。匯入僅執行一次;後續啟動會略過。
|
||||
|
||||
遙測注意事項:內嵌模式與其他任何設定一樣,會繼承映像檔的分析預設值。已發布的映像檔預設開啟分析;以 `--build-arg SNAPOTTER_ANALYTICS=off` 建置,或使用 app 內的管理員退出選項,即可停用它。
|
||||
|
||||
### 處理限制 {#processing-limits}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `MAX_UPLOAD_SIZE_MB` | `100` | 每次上傳的最大檔案大小(MB)。設為 0 為無上限。 |
|
||||
| `MAX_BATCH_SIZE` | `100` | 單一批次請求中的最大檔案數。設為 0 為無上限。 |
|
||||
| `CONCURRENT_JOBS` | `0`(自動) | 平行執行的批次工作數。設為 0 可根據可用 CPU 核心自動偵測。 |
|
||||
| `MAX_MEGAPIXELS` | `0`(無上限) | 允許的最大影像解析度(百萬像素)。設為 0 為無上限。 |
|
||||
| `MAX_WORKER_THREADS` | `0`(自動) | 影像處理的最大工作執行緒。設為 0 可根據可用 CPU 核心自動偵測。 |
|
||||
| `PROCESSING_TIMEOUT_S` | `0`(無限制) | 每個請求的最大處理時間(秒)。設為 0 為無逾時。 |
|
||||
| `MAX_PIPELINE_STEPS` | `20` | 管線中的最大步驟數。設為 0 為無限制。 |
|
||||
| `MAX_CANVAS_PIXELS` | `0`(無限制) | 輸出影像的最大畫布尺寸(像素)。設為 0 為無限制。 |
|
||||
| `MAX_SVG_SIZE_MB` | `0`(無上限) | 最大 SVG 檔案大小(MB)。設為 0 為無上限。 |
|
||||
| `MAX_SPLIT_GRID` | `100` | 影像分割工具的最大格線維度。 |
|
||||
| `MAX_PDF_PAGES` | `0`(無上限) | PDF 轉影像時的最大 PDF 頁數。設為 0 為無上限。 |
|
||||
|
||||
### 清理 {#cleanup}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `FILE_MAX_AGE_HOURS` | `72` | 未儲存的處理結果(原始上傳與工具輸出)在自動刪除前保留多久。你明確儲存至 Files 資料庫的檔案不受影響,會保留至你刪除它們為止。 |
|
||||
| `CLEANUP_INTERVAL_MINUTES` | `60` | 清理工作的執行頻率。 |
|
||||
|
||||
### 外觀 {#appearance}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `DEFAULT_THEME` | `light` | 新工作階段的預設主題。`light` 或 `dark`。 |
|
||||
| `DEFAULT_LOCALE` | `en` | 預設介面語言。 |
|
||||
| `DEFAULT_TOOL_VIEW` | `sidebar` | 預設工具版面。`sidebar` 或 `fullscreen`。 |
|
||||
|
||||
### Docker 權限 {#docker-permissions}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `PUID` | `999` | 以此 UID 執行容器程序。設為與你的主機使用者相符以供 bind mount 使用(`id -u`)。 |
|
||||
| `PGID` | `999` | 以此 GID 執行容器程序。設為與你的主機群組相符以供 bind mount 使用(`id -g`)。 |
|
||||
|
||||
## Docker 範例 {#docker-example}
|
||||
|
||||
```yaml
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest
|
||||
ports:
|
||||
- "1349:1349"
|
||||
volumes:
|
||||
- SnapOtter-data:/data
|
||||
- SnapOtter-workspace:/tmp/workspace
|
||||
environment:
|
||||
- AUTH_ENABLED=true
|
||||
- DEFAULT_USERNAME=admin
|
||||
- DEFAULT_PASSWORD=changeme
|
||||
- DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- MAX_UPLOAD_SIZE_MB=200
|
||||
- CONCURRENT_JOBS=4
|
||||
- FILE_MAX_AGE_HOURS=12
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_USER: snapotter
|
||||
POSTGRES_PASSWORD: snapotter
|
||||
POSTGRES_DB: snapotter
|
||||
volumes:
|
||||
- SnapOtter-pgdata:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U snapotter"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- SnapOtter-redisdata:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
volumes:
|
||||
SnapOtter-data:
|
||||
SnapOtter-workspace:
|
||||
SnapOtter-pgdata:
|
||||
SnapOtter-redisdata:
|
||||
```
|
||||
|
||||
## 磁碟區 {#volumes}
|
||||
|
||||
Docker Compose 堆疊使用四個磁碟區:
|
||||
|
||||
- `/data`(app)- AI 模型、Python venv 與使用者檔案。掛載此磁碟區以在重新啟動後保留已上傳的檔案與已安裝的 AI 套件包。
|
||||
- `/tmp/workspace`(app)- 處理中檔案的暫存空間。這可以是暫時性的,但掛載它可避免填滿容器的可寫入層。
|
||||
- `SnapOtter-pgdata`(postgres)- PostgreSQL 資料目錄。這保存所有關聯式資料(使用者、設定、管線、工作、稽核日誌)。透過 `pg_dump` 或磁碟區快照備份。
|
||||
- `SnapOtter-redisdata`(redis)- 用於持久化工作佇列的 Redis append-only 檔案。
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
description: "如何為 SnapOtter 做出貢獻。錯誤回報、功能請求、拉取請求以及 CLA 要求。"
|
||||
i18n_source_hash: 528802503035
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 28b8b8d90d65
|
||||
---
|
||||
|
||||
# 貢獻 {#contributing}
|
||||
|
||||
感謝你有興趣做出貢獻。本指南涵蓋如何參與、我們接受哪些內容,以及如何開始。
|
||||
|
||||
## 貢獻方式 {#ways-to-contribute}
|
||||
|
||||
### 議題(無需設定環境) {#issues-no-setup-required}
|
||||
|
||||
- **錯誤回報** - 有東西壞了?開一個附上重現步驟的[錯誤回報](https://github.com/snapotter-hq/snapotter/issues/new?template=bug_report.yml)。
|
||||
- **功能請求** - 有想法?發起一則[討論](https://github.com/snapotter-hq/snapotter/discussions/new?category=ideas),讓社群可以參與評估並投票支持。
|
||||
- **翻譯問題** - 發現錯誤或缺漏的翻譯?開一個[翻譯議題](https://github.com/snapotter-hq/snapotter/issues/new?template=translation.yml)。
|
||||
- **文件問題** - 文件裡有不對勁的地方?開一個[文件議題](https://github.com/snapotter-hq/snapotter/issues/new?template=documentation.yml)。
|
||||
|
||||
### 程式碼(需要 CLA) {#code-requires-cla}
|
||||
|
||||
我們接受以下類型的拉取請求:
|
||||
|
||||
| 類型 | 流程 |
|
||||
|------|---------|
|
||||
| 錯誤修正 | 直接開一個 PR(若已有對應議題,請附上連結) |
|
||||
| 新增翻譯 | 直接開一個 PR(見[翻譯指南](/zh-TW/guide/translations)) |
|
||||
| 文件改進 | 直接開一個 PR |
|
||||
| 測試覆蓋率改進 | 直接開一個 PR |
|
||||
| 新工具或新功能 | 先發起一則[討論](https://github.com/snapotter-hq/snapotter/discussions/new?category=ideas);維護者會在你動手寫程式碼之前,把獲准的想法轉為受追蹤的議題 |
|
||||
| 重構或架構變更 | 先發起一則[討論](https://github.com/snapotter-hq/snapotter/discussions/new?category=ideas),並在寫程式碼之前等待維護者的同意 |
|
||||
|
||||
### 我們不接受哪些內容 {#what-we-will-not-accept}
|
||||
|
||||
- 對 CI/CD 工作流程、發布設定,或 linter/compiler 設定的變更
|
||||
- 未簽署[貢獻者授權合約](#contributor-license-agreement)的 PR
|
||||
- 變更超過 400 行的 PR(請把大型工作拆成較小的 PR)
|
||||
- 未經事先討論並獲准的功能
|
||||
- 未經事先討論就變更 `packages/ai/`
|
||||
|
||||
## 貢獻者授權合約 {#contributor-license-agreement}
|
||||
|
||||
在我們合併你的第一個 PR 之前,你必須簽署我們的[個人 CLA](https://github.com/snapotter-hq/snapotter/blob/main/CLA.md)。這是一次性的要求。
|
||||
|
||||
**原因:** SnapOtter 採用雙重授權(AGPLv3 + 商業授權)。CLA 賦予我們在這兩種授權下散布你貢獻內容的權利。你仍保有作品完整的著作權。
|
||||
|
||||
**做法:** 當你開出第一個 PR 時,CLA Assistant 機器人會留言附上連結。點擊它、閱讀合約,並用你的 GitHub 帳號簽署。只需 30 秒。
|
||||
|
||||
如果你是代表雇主做出貢獻,且雇主保有你作品的智慧財產權,請在提交前透過 contact@snapotter.com 聯絡我們,安排企業 CLA。
|
||||
|
||||
## 開始 {#getting-started}
|
||||
|
||||
### 先決條件 {#prerequisites}
|
||||
|
||||
- Node.js 22+
|
||||
- pnpm 9+
|
||||
- Python 3.11+(僅供 AI 工具使用)
|
||||
- Docker(選用,供完整整合測試使用)
|
||||
|
||||
### 設定 {#setup}
|
||||
|
||||
```bash
|
||||
# Fork and clone
|
||||
git clone https://github.com/<your-username>/snapotter.git
|
||||
cd snapotter
|
||||
|
||||
# Start Postgres + Redis for local dev
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start dev servers (web on :1349, API on :13490)
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 執行檢查 {#running-checks}
|
||||
|
||||
在提交 PR 之前,請確保所有檢查在本機都能通過:
|
||||
|
||||
```bash
|
||||
pnpm lint # Biome lint + format check
|
||||
pnpm typecheck # TypeScript across monorepo
|
||||
pnpm test # Vitest unit + integration tests
|
||||
```
|
||||
|
||||
## 拉取請求流程 {#pull-request-process}
|
||||
|
||||
1. Fork 儲存庫,並從 `main` 建立一個分支(`feat/my-feature` 或 `fix/issue-123`)
|
||||
2. 使用[慣例式提交](https://www.conventionalcommits.org/),以聚焦、易於審查的提交進行變更
|
||||
3. 為你的變更新增或更新測試
|
||||
4. 在本機執行 `pnpm lint && pnpm typecheck && pnpm test`
|
||||
5. 針對 `main` 開一個 PR 並填寫範本
|
||||
6. 若被提示,簽署 CLA
|
||||
7. 等待 CI 通過並由維護者審查
|
||||
|
||||
### 審查預期 {#review-expectations}
|
||||
|
||||
- 我們的目標是在 7 天內回應 PR
|
||||
- 小而聚焦的 PR 會更快獲得審查
|
||||
- 若你在 7 天內未收到回覆,請在該討論串留言 ping 一下
|
||||
- 我們可能會請求變更、建議不同的做法,或在 PR 不符合專案方向時關閉它
|
||||
|
||||
### PR 合併之後 {#after-your-pr-is-merged}
|
||||
|
||||
你的貢獻會納入下一個版本,並在變更日誌中列名致謝。
|
||||
|
||||
## 適合新手的議題 {#good-first-issues}
|
||||
|
||||
想找點事做?看看我們的[適合新手的議題](https://github.com/snapotter-hq/snapotter/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22),那些是對初學者友善的任務;或看看[徵求協助](https://github.com/snapotter-hq/snapotter/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22),那些是我們樂見社群協助的較大項目。
|
||||
|
||||
## 程式碼風格 {#code-style}
|
||||
|
||||
- Biome 處理格式化與 linting(雙引號、分號、2 格縮排)
|
||||
- 提交前掛勾(pre-commit hook)會自動在暫存的檔案上執行 `biome check --write`
|
||||
- 若 linter 有意見,請修正程式碼(不要修改 Biome 設定)
|
||||
- 所有工作區一律使用 ES 模組(`import`/`export`)
|
||||
- 慣例式提交:`feat:`、`fix:`、`refactor:`、`docs:`、`test:`、`chore:`
|
||||
|
||||
完整的架構細節,請見[開發者指南](/zh-TW/guide/developer)。
|
||||
|
||||
## 安全性 {#security}
|
||||
|
||||
**請勿為安全漏洞開立公開的 PR 或議題。**請透過 [GitHub Security Advisories](https://github.com/snapotter-hq/snapotter/security/advisories/new) 或電子郵件 contact@snapotter.com 私下回報。完整細節請見 [SECURITY.md](https://github.com/snapotter-hq/snapotter/blob/main/SECURITY.md)。
|
||||
|
||||
## 有疑問? {#questions}
|
||||
|
||||
- [文件](https://docs.snapotter.com/)
|
||||
- [Discord](https://discord.gg/hr3s7HPUsr)
|
||||
- [GitHub Discussions](https://github.com/snapotter-hq/snapotter/discussions)
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
description: "SnapOtter 的 PostgreSQL 資料庫結構、資料表、遷移,以及備份程序。"
|
||||
i18n_source_hash: b37398ae91a3
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 34debcc24c5a
|
||||
---
|
||||
|
||||
# 資料庫 {#database}
|
||||
|
||||
SnapOtter 使用 PostgreSQL 17 搭配 [Drizzle ORM](https://orm.drizzle.team/)(pg-core/node-postgres)來持久化資料。結構定義於 `apps/api/src/db/schema.ts`。
|
||||
|
||||
連線透過 `DATABASE_URL` 環境變數設定(預設為 `postgres://snapotter:snapotter@postgres:5432/snapotter`)。在 Docker Compose 中,Postgres 容器把資料儲存在 `SnapOtter-pgdata` 具名磁碟區。
|
||||
|
||||
## 資料表 {#tables}
|
||||
|
||||
### users {#users}
|
||||
|
||||
儲存使用者帳號。首次執行時,會自動從 `DEFAULT_USERNAME` 與 `DEFAULT_PASSWORD` 建立。
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `id` | uuid | 主鍵 |
|
||||
| `username` | varchar | 唯一、必填 |
|
||||
| `passwordHash` | varchar | scrypt 雜湊 |
|
||||
| `role` | varchar | `admin`、`editor` 或 `user` |
|
||||
| `mustChangePassword` | boolean | 強制重設密碼旗標 |
|
||||
| `createdAt` | timestamp | 建立時間 |
|
||||
| `updatedAt` | timestamp | 最後更新時間 |
|
||||
|
||||
### sessions {#sessions}
|
||||
|
||||
有效的登入工作階段。每一列把一個工作階段權杖繫結到一位使用者。
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `id` | varchar | 主鍵(工作階段權杖) |
|
||||
| `userId` | uuid | 指向 `users.id` 的外鍵 |
|
||||
| `expiresAt` | timestamp | 到期時間 |
|
||||
| `createdAt` | timestamp | 建立時間 |
|
||||
|
||||
### teams {#teams}
|
||||
|
||||
用於組織使用者的群組。管理員可以把使用者指派到團隊。
|
||||
|
||||
| 欄位 | 型別 | 描述 |
|
||||
|--------|------|-------------|
|
||||
| `id` | uuid | 主鍵 |
|
||||
| `name` | varchar(唯一,最多 50 個字元) | 團隊名稱 |
|
||||
| `createdAt` | timestamp | 建立時間 |
|
||||
|
||||
### api_keys {#api-keys}
|
||||
|
||||
供程式化存取使用的 API 金鑰。原始金鑰只在建立時顯示一次;僅儲存其雜湊值。
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `id` | uuid | 主鍵 |
|
||||
| `userId` | uuid | 指向 `users.id` 的外鍵 |
|
||||
| `keyHash` | varchar | 金鑰的 scrypt 雜湊 |
|
||||
| `name` | varchar | 使用者提供的標籤 |
|
||||
| `createdAt` | timestamp | 建立時間 |
|
||||
| `lastUsedAt` | timestamp | 每次通過驗證的請求時更新 |
|
||||
|
||||
金鑰以 `si_` 為前綴,後接 96 個十六進位字元(48 個隨機位元組)。
|
||||
|
||||
### pipelines {#pipelines}
|
||||
|
||||
使用者在 UI 中建立的已儲存工具鏈。
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `id` | uuid | 主鍵 |
|
||||
| `name` | varchar | 管線名稱 |
|
||||
| `description` | varchar | 選填的描述 |
|
||||
| `steps` | jsonb | `{ toolId, settings }` 物件的陣列 |
|
||||
| `createdAt` | timestamp | 建立時間 |
|
||||
|
||||
### user_files {#user-files}
|
||||
|
||||
具有版本鏈追蹤的持久化檔案庫。每個儲存結果的處理步驟都會建立一個新列,透過 `parentId` 連結到它的父列,形成一棵版本樹。
|
||||
|
||||
| 欄位 | 型別 | 描述 |
|
||||
|--------|------|-------------|
|
||||
| `id` | uuid | 主鍵 |
|
||||
| `userId` | uuid | 指向 users 的外鍵(CASCADE DELETE) |
|
||||
| `originalName` | varchar | 原始上傳檔名 |
|
||||
| `storedName` | varchar | 磁碟上的檔名 |
|
||||
| `mimeType` | varchar | MIME 類型 |
|
||||
| `size` | integer | 檔案大小(位元組) |
|
||||
| `width` | integer | 影像寬度(px) |
|
||||
| `height` | integer | 影像高度(px) |
|
||||
| `version` | integer | 版本編號(1 = 原始) |
|
||||
| `parentId` | uuid 或 null | 指向 user_files 的外鍵(父版本) |
|
||||
| `toolChain` | jsonb | 依序套用以產生此版本的工具 ID |
|
||||
| `createdAt` | timestamp | 建立時間 |
|
||||
|
||||
### jobs {#jobs}
|
||||
|
||||
追蹤處理工作,以進行進度回報與清理。
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `id` | uuid | 主鍵 |
|
||||
| `type` | varchar | 工具或管線識別碼 |
|
||||
| `status` | varchar | `queued`、`processing`、`completed` 或 `failed` |
|
||||
| `progress` | real | 0.0-1.0 的比例 |
|
||||
| `inputFiles` | jsonb | 輸入檔案路徑的陣列 |
|
||||
| `outputPath` | varchar | 結果檔案的路徑 |
|
||||
| `settings` | jsonb | 使用的工具設定 |
|
||||
| `error` | varchar | 失敗時的錯誤訊息 |
|
||||
| `createdAt` | timestamp | 建立時間 |
|
||||
| `completedAt` | timestamp | 完成時間 |
|
||||
|
||||
### settings {#settings}
|
||||
|
||||
供管理員可從 UI 變更的伺服器層級設定的鍵值儲存區。
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `key` | varchar | 主鍵 |
|
||||
| `value` | varchar | 設定值 |
|
||||
| `updatedAt` | timestamp | 最後更新時間 |
|
||||
|
||||
### roles {#roles}
|
||||
|
||||
具有細緻權限的自訂角色。
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `id` | uuid | 主鍵 |
|
||||
| `name` | varchar | 唯一角色名稱 |
|
||||
| `description` | varchar | 選填的描述 |
|
||||
| `permissions` | jsonb | 權限字串的陣列 |
|
||||
| `createdAt` | timestamp | 建立時間 |
|
||||
|
||||
### audit_log {#audit-log}
|
||||
|
||||
與安全性相關的動作記錄。
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `id` | uuid | 主鍵 |
|
||||
| `userId` | uuid | 指向 users 的外鍵 |
|
||||
| `action` | varchar | 動作類型 |
|
||||
| `details` | jsonb | 動作特定的資料 |
|
||||
| `createdAt` | timestamp | 動作時間 |
|
||||
|
||||
## 遷移 {#migrations}
|
||||
|
||||
Drizzle 處理結構遷移。遷移檔案位於 `apps/api/drizzle/`。在開發期間:
|
||||
|
||||
```bash
|
||||
cd apps/api
|
||||
npx drizzle-kit generate # generate a migration from schema changes
|
||||
npx drizzle-kit migrate # apply pending migrations
|
||||
```
|
||||
|
||||
在生產環境中,待處理的遷移會在啟動時自動套用。
|
||||
|
||||
## 備份與還原 {#backup-and-restore}
|
||||
|
||||
關聯式資料庫位於 Postgres 容器的 `SnapOtter-pgdata` 磁碟區,而非應用程式的 `/data` 磁碟區。
|
||||
|
||||
**選項 1:pg_dump(建議)**
|
||||
|
||||
```bash
|
||||
# Dump the database while the stack is running
|
||||
docker exec SnapOtter-postgres pg_dump -U snapotter snapotter > backup.sql
|
||||
|
||||
# Restore into a fresh database
|
||||
cat backup.sql | docker exec -i SnapOtter-postgres psql -U snapotter snapotter
|
||||
```
|
||||
|
||||
**選項 2:磁碟區快照**
|
||||
|
||||
```bash
|
||||
# Stop the stack, then snapshot the pgdata volume
|
||||
docker compose down
|
||||
docker run --rm -v SnapOtter-pgdata:/data -v $(pwd)/backup:/backup \
|
||||
alpine tar czf /backup/snapotter-pgdata.tar.gz -C /data .
|
||||
```
|
||||
|
||||
### 從 1.x(SQLite)遷移 {#migrating-from-1-x-sqlite}
|
||||
|
||||
從 SnapOtter 1.x 升級有其專屬指南:見[從 1.x 升級到 2.0](./upgrading)。簡而言之,重複使用你既有的 `/data` 磁碟區,2.0 會在首次開機時自動偵測並匯入 `/data/snapotter.db`(或設定 `SQLITE_MIGRATE_PATH` 明確指向它)。請先備份整個 `/data` 磁碟區,而不只是 `snapotter.db`:1.x 使用 SQLite 的 WAL 模式,所以一個已停止的容器往往會把它大部分的資料留在 `snapotter.db-wal` 中,旁邊只有一個幾乎空白的 `snapotter.db`。
|
||||
@@ -0,0 +1,571 @@
|
||||
---
|
||||
description: "使用 Docker 將 SnapOtter 部署到正式環境。硬體需求、GPU 設定,以及 Nginx、Traefik 和 Cloudflare 的反向代理設定。"
|
||||
i18n_source_hash: 6b6957060fa6
|
||||
i18n_provenance: machine
|
||||
i18n_output_hash: 2877802ecaa6
|
||||
---
|
||||
|
||||
# 部署 {#deployment}
|
||||
|
||||
SnapOtter 以 3 個容器的 Docker Compose 堆疊部署:SnapOtter 應用程式映像檔、PostgreSQL 17 和 Redis 8。應用程式映像檔支援 **linux/amd64**(搭配 NVIDIA CUDA 進行 AI 加速)與 **linux/arm64**(CPU),因此可原生執行於 Intel/AMD 伺服器、Apple Silicon Mac,以及 Raspberry Pi 4/5 等 ARM 裝置。目前不支援透過 VA-API、Quick Sync 或 OpenCL 進行 Intel/AMD iGPU 加速的 AI 推論。
|
||||
|
||||
關於 GPU 設定、Docker Compose 範例與版本鎖定,請參閱 [Docker Image](./docker-tags)。
|
||||
|
||||
## 快速開始(CPU) {#quick-start-cpu}
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml - Copy this file and run: docker compose up -d
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest # or ghcr.io/snapotter-hq/snapotter:latest
|
||||
container_name: SnapOtter
|
||||
ports:
|
||||
- "1349:1349" # Web UI + API
|
||||
volumes:
|
||||
- SnapOtter-data:/data # AI models, user files (PERSISTENT)
|
||||
- SnapOtter-workspace:/tmp/workspace # Temp processing files (can be tmpfs)
|
||||
environment:
|
||||
# --- Authentication ---
|
||||
- AUTH_ENABLED=true # Set to false to disable login entirely
|
||||
- DEFAULT_USERNAME=admin # First-run admin username
|
||||
- DEFAULT_PASSWORD=admin # First-run admin password (you'll be forced to change it)
|
||||
|
||||
# --- Database + Queue ---
|
||||
- DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
|
||||
- REDIS_URL=redis://redis:6379
|
||||
|
||||
# --- Limits (set 0 for unlimited) ---
|
||||
# - MAX_UPLOAD_SIZE_MB=100 # Per-file upload limit in MB
|
||||
# - MAX_BATCH_SIZE=100 # Max files per batch request
|
||||
# - RATE_LIMIT_PER_MIN=1000 # API rate limit per IP, default shown (0 = disabled)
|
||||
# - MAX_USERS=0 # Max user accounts
|
||||
|
||||
# --- Networking ---
|
||||
# - TRUST_PROXY=true # Trust X-Forwarded-For headers (set false if not behind a proxy)
|
||||
|
||||
# --- Bind mount permissions ---
|
||||
# - PUID=1000 # Match your host user's UID (run: id -u)
|
||||
# - PGID=1000 # Match your host user's GID (run: id -g)
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:1349/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 60s
|
||||
retries: 3
|
||||
shm_size: "2gb" # Needed for Python ML shared memory
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: SnapOtter-postgres
|
||||
environment:
|
||||
POSTGRES_USER: snapotter
|
||||
POSTGRES_PASSWORD: snapotter # Change this for non-local deployments
|
||||
POSTGRES_DB: snapotter
|
||||
volumes:
|
||||
- SnapOtter-pgdata:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U snapotter"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 15s
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
container_name: SnapOtter-redis
|
||||
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- SnapOtter-redisdata:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
SnapOtter-data: # Named volume - Docker manages permissions automatically
|
||||
SnapOtter-workspace:
|
||||
SnapOtter-pgdata:
|
||||
SnapOtter-redisdata:
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
之後即可在 `http://localhost:1349` 存取應用程式。
|
||||
|
||||
> **遇到 Docker Hub 速率限制?** 將 `snapotter/snapotter:latest` 換成 `ghcr.io/snapotter-hq/snapotter:latest`,改從 GitHub Container Registry 拉取。兩個登錄檔在每次發行時都會收到相同的映像檔。
|
||||
|
||||
## 快速開始(NVIDIA CUDA) {#quick-start-nvidia-cuda}
|
||||
|
||||
若要在 AI 工具(去背、放大、臉部強化、OCR)上使用 NVIDIA CUDA 加速:
|
||||
|
||||
```yaml
|
||||
# docker-compose-gpu.yml - Requires: NVIDIA GPU + nvidia-container-toolkit
|
||||
# Install toolkit: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest
|
||||
container_name: SnapOtter
|
||||
ports:
|
||||
- "1349:1349"
|
||||
volumes:
|
||||
- SnapOtter-data:/data
|
||||
- SnapOtter-workspace:/tmp/workspace
|
||||
environment:
|
||||
- AUTH_ENABLED=true
|
||||
- DEFAULT_USERNAME=admin
|
||||
- DEFAULT_PASSWORD=admin
|
||||
- DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:1349/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 60s
|
||||
retries: 3
|
||||
shm_size: "2gb" # Required for PyTorch CUDA shared memory
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all # Or set to 1 for a specific GPU
|
||||
capabilities: [gpu]
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: SnapOtter-postgres
|
||||
environment:
|
||||
POSTGRES_USER: snapotter
|
||||
POSTGRES_PASSWORD: snapotter
|
||||
POSTGRES_DB: snapotter
|
||||
volumes:
|
||||
- SnapOtter-pgdata:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U snapotter"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 15s
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
container_name: SnapOtter-redis
|
||||
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- SnapOtter-redisdata:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
SnapOtter-data:
|
||||
SnapOtter-workspace:
|
||||
SnapOtter-pgdata:
|
||||
SnapOtter-redisdata:
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-gpu.yml up -d
|
||||
```
|
||||
|
||||
在記錄中確認 CUDA 是否偵測到:
|
||||
|
||||
```bash
|
||||
docker logs SnapOtter 2>&1 | head -20
|
||||
# Look for: [gpu] CUDA available via torch
|
||||
```
|
||||
|
||||
## 硬體需求 {#hardware-requirements}
|
||||
|
||||
這些數字來自跨多種系統的效能測試,從搭載 NVIDIA RTX 4070 的現代 amd64 工作站,一直到 Raspberry Pi,在每台機器上執行整個工具目錄,並掃描 Docker 資源限制以找出實際的下限。
|
||||
|
||||
### 快速參考 {#quick-reference}
|
||||
|
||||
| 等級 | 使用情境 | CPU | RAM | GPU | 儲存空間 |
|
||||
|------|----------|-----|-----|-----|---------|
|
||||
| 最低 | 影像、檔案與輕量 PDF 工具;單一使用者;小批次 | 2 核心 | 2 GB | 無 | ~7 GB |
|
||||
| 建議 | 全部五種模態,包含影片、PDF 與 CPU 上的 AI;批次;少數使用者 | 4 核心 | 4 GB | 無 | ~25 GB |
|
||||
| 完整 | 全部高速運作,包含 GPU AI;大量批次;多位使用者 | 6-8 核心 | 8 GB | NVIDIA 8 GB 以上 VRAM(12 GB 較充裕) | ~35 GB |
|
||||
|
||||
**架構:僅限 64 位元**(`linux/amd64` 或 `linux/arm64`)。SnapOtter 可原生執行於 Intel/AMD 伺服器、Apple Silicon Mac,以及 64 位元 ARM 板,包括 **Raspberry Pi 4 和 5**(4-8 GB)。它**無法**執行於 32 位元 ARM(`armv7`/`armhf`),因為沒有為其建置映像檔,也無法執行於 Pi Zero 這類 512 MB 等級的板子,因為它們低於記憶體下限(見下文)。
|
||||
|
||||
### 最低(影像、檔案與輕量 PDF 工具;無 AI) {#minimum-image-files-and-light-pdf-tools-no-ai}
|
||||
|
||||
| 資源 | 需求 |
|
||||
|---|---|
|
||||
| CPU | 2 核心 |
|
||||
| RAM | 2 GB |
|
||||
| 磁碟 | ~5.5 GB(映像檔)+ 資料磁碟區 |
|
||||
| GPU | 非必要 |
|
||||
|
||||
全部 222 個非 AI 目錄工具,包括影像(調整大小、裁切、轉換、壓縮、調整、浮水印)、影片(修剪、靜音、重新封裝)、音訊(轉換、正規化、修剪)、PDF(合併、分割、壓縮、旋轉、保護)、檔案轉換,以及專屬的轉換預設,都能在一般硬體上執行。即使是大型檔案,多數作業也能在遠低於一秒內完成:一張 2.7 MB 的影像調整大小約需 ~0.05 秒,重新編碼為 WebP 約需 ~2 秒。
|
||||
|
||||
記憶體下限是真實存在的,這來自一次 Docker 資源限制掃描:**512 MB 無法啟動堆疊**(即使是單一影像調整大小也會被終止),**1 GB** 可處理單一檔案作業,但多檔案批次會耗盡記憶體,而 **2 GB / 2 核心** 是能舒適處理批次的最小組態。
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
```
|
||||
|
||||
**唯一吃重 CPU 的例外是影片重新編碼。** 串流複製作業(修剪、靜音、容器重新封裝)是瞬間完成的,但轉碼為不同的編解碼器則受限於 CPU。一段 1080p / 45 秒的影片重新編碼為 VP9(WebM),在快速的現代 CPU 上約需 **~40 秒**,在 Apple Silicon 上約 ~45 秒,在較舊的行動 4 核心上約 ~80 秒,在較舊的 4 核心伺服器上則需 **~130 秒**。如果你的工作負載偏重影片,請優先考慮 CPU 核心數與時脈速度,或提高容器的 `cpus:` 限制。隨附的 compose 預設將應用程式上限設為 4 核心(GPU compose 為 8 核心)。
|
||||
|
||||
### 建議(CPU 上的 AI 工具) {#recommended-ai-tools-on-cpu}
|
||||
|
||||
| 資源 | 需求 |
|
||||
|---|---|
|
||||
| CPU | 4 核心 |
|
||||
| RAM | 4 GB |
|
||||
| 磁碟 | 3 GB(映像檔)+ 24 GB(AI 模型)+ 工作空間 |
|
||||
| GPU | 非必要(CPU 後備) |
|
||||
|
||||
**安裝 AI 套件組是把 RAM 推升到 4 GB 的原因。** 在未安裝 AI 時,應用程式閒置時約佔用 360 MB;安裝全部七個套件組後,常駐記憶體維持在 ~2.6 GB,因為 Python AI sidecar 會在啟動時預先載入其模型(去背、放大、OCR、轉錄、臉部偵測、修復)。非 AI 安裝維持輕量;AI 安裝則需要 ≥4 GB。
|
||||
|
||||
多數 AI 工具在 CPU 上完全可用;少數確實需要 GPU。在現代 4 核心 CPU 上測得:
|
||||
|
||||
| AI 工具 | CPU 時間 | CPU 上可用? |
|
||||
|---|---|---|
|
||||
| 臉部偵測(模糊臉部、智慧裁切、紅眼)、雜訊移除 | 不到 1 秒 | 是 |
|
||||
| OCR、轉錄、字幕 | 1-3 秒 | 是 |
|
||||
| 上色、臉部強化 | ~10 秒 | 是 |
|
||||
| 去背 / 替換 / 模糊 | ~29 秒 | 是(需等待) |
|
||||
| AI 放大(RealESRGAN) | 小圖 ~33 秒;大圖需數分鐘 | 勉強可用 — 強烈建議 GPU |
|
||||
| 相片修復(完整流程) | 數分鐘 | 否 — 需要 GPU 或快速的多核心 CPU |
|
||||
|
||||
SnapOtter 刻意不將這些模型下載內建於 Docker 映像檔中。AI 套件組僅在管理員啟用相關工具時才會拉取,儲存於持久化的 `/data/ai` 磁碟區,並由所有依賴相同模型堆疊的工具共用。這讓最終容器映像檔保持精簡,同時仍允許完整的 AI 安裝達到下方較大的儲存數字。
|
||||
|
||||
有些工具依賴不只一個共用套件組。舉例來說,證件照同時需要 `background-removal` 和 `face-detection`;如果 `background-removal` 已安裝,啟用證件照只會下載缺少的 `face-detection` 套件組。相同的重用機制適用於所有 AI 工具。
|
||||
|
||||
AI 模型下載大小:
|
||||
|
||||
| 套件組 | 磁碟大小 |
|
||||
|---|---|
|
||||
| 去背 | 4-5 GB |
|
||||
| 放大 + 臉部強化 + 雜訊移除 | 5-6 GB |
|
||||
| 臉部偵測 | 200-300 MB |
|
||||
| 物件消除 + 上色 | 1-2 GB |
|
||||
| OCR | 5-6 GB |
|
||||
| 相片修復 | 4-5 GB |
|
||||
| **全部套件組** | **~24 GB** |
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '4'
|
||||
memory: 4G
|
||||
```
|
||||
|
||||
### 完整(NVIDIA CUDA 上的 AI 工具) {#full-ai-tools-on-nvidia-cuda}
|
||||
|
||||
| 資源 | 需求 |
|
||||
|---|---|
|
||||
| CPU | 6-8 核心(即使使用 GPU AI,影片前置處理與並行仍在 CPU 上執行) |
|
||||
| RAM | 8 GB |
|
||||
| GPU | NVIDIA,8 GB 以上 VRAM(建議 12 GB) |
|
||||
| 磁碟 | 總計 ~35 GB |
|
||||
|
||||
NVIDIA GPU(CUDA)能大幅加速吃重的 AI 模型。在 RTX 4070 與現代 CPU 上測得:
|
||||
|
||||
| AI 工具 | GPU 加速倍數 | 備註 |
|
||||
|---|---|---|
|
||||
| AI 放大(RealESRGAN 2×) | **~47×** | 最大的收益 — 不到一秒,對比 ~33 秒(大圖需數分鐘) |
|
||||
| 臉部強化(CodeFormer) | **~12×** | ~0.9 秒對比 ~11 秒 |
|
||||
| 轉錄(Whisper) | ~4.5× | |
|
||||
| 去背 / 替換 / 模糊 | ~4× | GPU 上 ~7 秒對比 CPU 上 ~29 秒 |
|
||||
| 上色 | ~1.8× | |
|
||||
| OCR、臉部偵測、紅眼、雜訊移除 | ~1× | 在 CPU 上已很快 — GPU 幫不上忙 |
|
||||
| 相片修復 | 無 | 即使在 GPU 上也受限於 CPU(0% GPU 使用率);此處快速的 CPU 比 GPU 更重要 |
|
||||
|
||||
值得使用 GPU 的工具是 **放大、臉部強化、轉錄和去背**。臉部偵測、OCR 和紅眼受限於 CPU 且已經很快,因此 GPU 幫不上忙。
|
||||
|
||||
在放大搭配臉部強化時,VRAM 峰值使用量達到 7.5 GB。6 GB 的 NVIDIA GPU 對多數個別 AI 工具都能運作,但在放大時會失敗。8-12 GB VRAM 可處理所有情況。
|
||||
|
||||
目前不支援透過 VA-API、Quick Sync 或 OpenCL 進行 Intel/AMD iGPU 加速的 AI 推論。將 `/dev/dri` 對應到容器中並不會啟用 AI GPU 加速;除非有 NVIDIA CUDA 可用,否則 SnapOtter 會在 CPU 上執行 AI 工具。
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '4'
|
||||
memory: 8G
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
```
|
||||
|
||||
### 並行使用者 {#concurrent-users}
|
||||
|
||||
對預設限制 4 核心的應用程式容器發出的並行影像調整大小請求:
|
||||
|
||||
| 並行請求數 | 平均回應時間 | 錯誤 |
|
||||
|---|---|---|
|
||||
| 1 | 0.4s | 0 |
|
||||
| 5 | 1.2s | 0 |
|
||||
| 10 | 2.1s | 0 |
|
||||
|
||||
隨著工作者集區飽和,回應時間以次線性方式劣化,且無錯誤。提高應用程式容器的 `cpus:` 限制(或使用核心數更多的主機)可提升上限。請注意,吃重的作業(影片轉碼、CPU AI)在整個執行期間都會佔住一個工作者,因此請依你預期的並行吃重作業數量來調整 CPU,而不只是依請求數。
|
||||
|
||||
### 支援的影像格式 {#supported-image-formats}
|
||||
|
||||
SnapOtter 支援 **55+ 種輸入格式** 與 **14 種輸出格式**,包括來自 20+ 相機品牌的 RAW 檔、專業格式(PSD、EPS、OpenEXR、HDR)、現代編解碼器(JPEG XL、AVIF、HEIC、QOI),以及科學/遊戲格式(FITS、DDS)。
|
||||
|
||||
關於每種支援格式、使用的解碼器與可用的品質控制的詳細資訊,請參閱[完整格式清單](/zh-TW/guide/supported-formats)。
|
||||
|
||||
### 已知限制 {#known-limitations}
|
||||
|
||||
- **內容感知調整大小**在大型影像(>5 MP)上會因 caire 二進位檔的限制而當機。對較小的影像運作正常。
|
||||
- **HEIF 解碼**需要 13-23 秒。HEIC(Apple 的變體)快得多,僅需 0.3-0.9 秒。
|
||||
- **OCR 日文**在 CPU 上會因 PaddlePaddle MKLDNN 的錯誤而失敗。在 GPU 上可運作。
|
||||
- **放大**在 CPU 上對超出小圖的任何影像都會逾時。實務使用需要 GPU。
|
||||
- **CodeFormer** 臉部強化明顯比 GFPGAN 慢(GPU 上 53 秒對比 2 秒)。多數使用情境建議使用 GFPGAN。
|
||||
|
||||
## 磁碟區 {#volumes}
|
||||
|
||||
| 掛載 / 磁碟區 | 用途 | 是否必要? |
|
||||
|---|---|---|
|
||||
| `/data`(app) | AI 模型、Python venv、使用者檔案 | **是** — 少了會遺失檔案 |
|
||||
| `/tmp/workspace`(app) | 暫存處理檔案(自動清理) | 建議 |
|
||||
| `SnapOtter-pgdata`(postgres) | PostgreSQL 資料目錄(使用者、設定、管線、作業) | **是** — 少了會遺失資料 |
|
||||
| `SnapOtter-redisdata`(redis) | Redis 僅附加檔案,用於持久化的作業佇列 | 建議 |
|
||||
|
||||
### 繫結掛載 vs. 具名磁碟區 {#bind-mounts-vs-named-volumes}
|
||||
|
||||
**具名磁碟區**(建議)— Docker 會自動管理權限:
|
||||
```yaml
|
||||
volumes:
|
||||
- SnapOtter-data:/data
|
||||
```
|
||||
|
||||
**繫結掛載** — 由你管理權限。設定 `PUID`/`PGID` 以符合你的主機使用者:
|
||||
```yaml
|
||||
volumes:
|
||||
- ./SnapOtter-data:/data
|
||||
environment:
|
||||
- PUID=1000 # Your host UID (run: id -u)
|
||||
- PGID=1000 # Your host GID (run: id -g)
|
||||
```
|
||||
|
||||
### 儲存權限 {#storage-permissions}
|
||||
|
||||
SnapOtter 在執行期間會寫入兩個位置:`/data`(使用者檔案、記錄、AI 模型與 Python venv)和 `/tmp/workspace`(暫存處理暫存區)。兩者都必須可由容器所執行的使用者寫入。若其中之一不可寫入,容器會在**啟動時快速失敗**,並顯示一則訊息,指出該目錄、執行中的 UID/GID,以及修正方式,而不是在啟動時看似「健康」,然後在第一次上傳時以難解的錯誤失敗。
|
||||
|
||||
權限的處理方式取決於容器的啟動方式:
|
||||
|
||||
**預設(以 root 啟動,降權至 `snapotter`)** — 進入點以 root 啟動,修正已掛載磁碟區的擁有權,然後透過 `gosu` 降權至非特權的 `snapotter` 使用者。具名磁碟區無需任何設定即可運作。對於繫結掛載,請將 `PUID`/`PGID` 設為你的主機使用者(如上),使它寫入的檔案由你擁有。
|
||||
|
||||
**Kubernetes / OpenShift(透過 `runAsUser` 以非 root 執行)** — 直接以非 root 使用者啟動時,容器無法自行 chown 磁碟區,因此協調器必須使其可寫入。請設定 `fsGroup`:
|
||||
|
||||
```yaml
|
||||
securityContext:
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
fsGroup: 999 # makes mounted volumes writable by the pod
|
||||
```
|
||||
|
||||
映像檔的可寫入目錄群組擁有者為 GID 0 且群組可寫入,因此以**任意 UID** 加上 root 補充群組(OpenShift 的預設值)執行的 pod 無需 `chown` 即可寫入。
|
||||
|
||||
**TrueNAS Scale(以及其他「外來 UID」設定)** — TrueNAS 以非 root 使用者執行應用程式(通常是 `568:568`),並掛載由不同使用者擁有的主機資料集,因此進入點和 `fsGroup` 都無法自行使其可寫入。請擇一:
|
||||
|
||||
- **以 root 執行應用程式**(建議)— 將應用程式的使用者保持未設定或設為 `0`,讓預設進入點修正權限並降權至 `snapotter`。
|
||||
- **以 UID `999` 執行** — 將應用程式的使用者/群組設為 `999:999`(SnapOtter 內建的 `snapotter` 使用者),使其符合映像檔的擁有權。
|
||||
- 從 TrueNAS shell 將主機資料集 **`chown`** 至容器所執行的 UID:
|
||||
|
||||
```bash
|
||||
# 使用啟動錯誤中的 UID(或在容器內執行 `id`)
|
||||
chown -R 568:568 /mnt/<pool>/<dataset>
|
||||
```
|
||||
|
||||
啟動錯誤會指出要使用的確切 UID,因此最快的做法是先啟動一次應用程式、讀取訊息,然後據此 `chown`(或調整使用者)。
|
||||
|
||||
## 環境變數 {#environment-variables}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `AUTH_ENABLED` | `true` | 啟用/停用登入需求 |
|
||||
| `DEFAULT_USERNAME` | `admin` | 初始管理員使用者名稱 |
|
||||
| `DEFAULT_PASSWORD` | `admin` | 初始管理員密碼(首次登入時強制變更) |
|
||||
| `MAX_UPLOAD_SIZE_MB` | `100` | 每個檔案的上傳限制 |
|
||||
| `MAX_BATCH_SIZE` | `100` | 每個批次請求的最大檔案數 |
|
||||
| `RATE_LIMIT_PER_MIN` | `1000` | 每個 IP 每分鐘的 API 請求數(設為 0 以停用) |
|
||||
| `MAX_USERS` | `0`(無限制) | 最大使用者帳號數 |
|
||||
| `TRUST_PROXY` | `true` | 信任來自反向代理的 X-Forwarded-For 標頭 |
|
||||
| `PUID` | `999` | 以此 UID 執行(用於繫結掛載權限) |
|
||||
| `PGID` | `999` | 以此 GID 執行(用於繫結掛載權限) |
|
||||
| `LOG_LEVEL` | `info` | 記錄詳細程度:fatal、error、warn、info、debug、trace |
|
||||
| `CONCURRENT_JOBS` | `0`(自動) | 最大並行 AI 處理作業數 |
|
||||
| `SESSION_DURATION_HOURS` | `168` | 登入工作階段存留期(7 天) |
|
||||
| `CORS_ORIGIN` | (空白) | 以逗號分隔的允許來源,或留空表示同源 |
|
||||
|
||||
## 健康檢查 {#health-check}
|
||||
|
||||
容器內建健康檢查:
|
||||
|
||||
```bash
|
||||
# Check container health status
|
||||
docker inspect --format='{{.State.Health.Status}}' SnapOtter
|
||||
|
||||
# Manual health check
|
||||
curl http://localhost:1349/api/v1/health
|
||||
# {"status":"healthy","version":"x.y.z"}
|
||||
```
|
||||
|
||||
## 反向代理 {#reverse-proxy}
|
||||
|
||||
SnapOtter 預設會設定 `TRUST_PROXY=true`,使速率限制與記錄使用來自 `X-Forwarded-For` 標頭的真實用戶端 IP。
|
||||
|
||||
### Nginx {#nginx}
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name images.example.com;
|
||||
|
||||
# Match MAX_UPLOAD_SIZE_MB (0 = nginx default 1M, so set high for unlimited)
|
||||
client_max_body_size 500M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:1349;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# SSE support (batch progress, feature install progress)
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nginx Proxy Manager {#nginx-proxy-manager}
|
||||
|
||||
1. 新增一個 Proxy Host
|
||||
2. 將 Domain Name 設為你的網域
|
||||
3. 將 Scheme 設為 `http`,Forward Hostname 設為 `SnapOtter`(或你的容器 IP),Forward Port 設為 `1349`
|
||||
4. 啟用 WebSocket 支援
|
||||
5. 在 Advanced 底下,加入:`client_max_body_size 500M;` 和 `proxy_buffering off;`
|
||||
|
||||
### Traefik {#traefik}
|
||||
|
||||
```yaml
|
||||
# Add these labels to the SnapOtter service in docker-compose.yml
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.snapotter.rule=Host(`images.example.com`)"
|
||||
- "traefik.http.routers.snapotter.entrypoints=websecure"
|
||||
- "traefik.http.routers.snapotter.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.snapotter.loadbalancer.server.port=1349"
|
||||
# Increase upload limit (default 2MB is too low)
|
||||
- "traefik.http.middlewares.snapotter-body.buffering.maxRequestBodyBytes=524288000"
|
||||
- "traefik.http.routers.snapotter.middlewares=snapotter-body"
|
||||
```
|
||||
|
||||
### Caddy {#caddy}
|
||||
|
||||
```txt
|
||||
images.example.com {
|
||||
reverse_proxy localhost:1349 {
|
||||
flush_interval -1
|
||||
transport http {
|
||||
read_timeout 300s
|
||||
write_timeout 300s
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`flush_interval -1` 會停用回應緩衝,這是 SSE 進度事件(批次處理、AI 工具、功能安裝)所必需的。延長的逾時允許大型檔案上傳在 Caddy 提早關閉連線之前完成。
|
||||
|
||||
### Cloudflare Tunnels {#cloudflare-tunnels}
|
||||
|
||||
```bash
|
||||
cloudflared tunnel --url http://localhost:1349
|
||||
```
|
||||
|
||||
注意:Cloudflare 在免費方案上有 100 MB 的上傳限制。請將 `MAX_UPLOAD_SIZE_MB=100` 設為相符。
|
||||
|
||||
## CI/CD {#ci-cd}
|
||||
|
||||
GitHub 儲存庫有三個工作流程:
|
||||
|
||||
- **ci.yml** — 在每次推送與 PR 時自動執行。進行 lint、型別檢查、測試、建置,並驗證 Docker 映像檔(不推送)。
|
||||
- **release.yml** — 透過 `workflow_dispatch` 手動觸發。執行 semantic-release 以建立版本標籤與 GitHub 發行,然後建置多架構 Docker 映像檔(amd64 + arm64)並推送到 Docker Hub(`snapotter/snapotter`)和 GitHub Container Registry(`ghcr.io/snapotter-hq/snapotter`)。
|
||||
- **deploy-docs.yml** — 建置這個文件網站,並在推送到 `main` 時部署到 Cloudflare Pages。
|
||||
|
||||
若要建立發行,請在 GitHub UI 中前往 **Actions > Release > Run workflow**,或執行:
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml
|
||||
```
|
||||
|
||||
Semantic-release 會依提交歷史決定版本。`latest` Docker 標籤永遠指向最近一次的發行。
|
||||
|
||||
## 分析 {#analytics}
|
||||
|
||||
SnapOtter 包含匿名的產品分析(工具使用模式、錯誤回報),以協助抓出錯誤並改善功能。它預設為開啟。你的檔案、檔案名稱與個人資料絕不屬於這些資料的一部分。停用分析後,SnapOtter 仍正常運作。
|
||||
|
||||
### 停用分析 {#disabling-analytics}
|
||||
|
||||
執行期間的退出是一鍵式的管理員切換。開啟 Settings > System > Privacy,關閉 Anonymous Product Analytics。它會立即對整個執行個體停止,無需重新建置。
|
||||
|
||||
若要取得一個絕不會發出分析的映像檔,請透過複製儲存庫並重新建置來設定建置時的硬性關閉:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/snapotter-hq/SnapOtter.git
|
||||
cd SnapOtter
|
||||
docker compose -f docker/docker-compose.yml build --build-arg SNAPOTTER_ANALYTICS=off
|
||||
docker compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
或將建置引數加入你現有的 `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
snapotter:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
args:
|
||||
SNAPOTTER_ANALYTICS: "off"
|
||||
```
|
||||
@@ -0,0 +1,234 @@
|
||||
---
|
||||
description: "SnapOtter 的本機開發環境設定、指令、程式碼慣例,以及如何新增工具。"
|
||||
i18n_source_hash: cb03724d2829
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 64a00be99b89
|
||||
---
|
||||
|
||||
# 開發者指南 {#developer-guide}
|
||||
|
||||
如何設定本機開發環境,並為 SnapOtter 貢獻程式碼。
|
||||
|
||||
## 先決條件 {#prerequisites}
|
||||
|
||||
- [Node.js](https://nodejs.org/) 22+
|
||||
- [pnpm](https://pnpm.io/) 9+(`corepack enable && corepack prepare pnpm@latest --activate`)
|
||||
- [Docker](https://www.docker.com/)(本機 Postgres + Redis、容器建置與 AI 功能所必需)
|
||||
- Git
|
||||
|
||||
只有在你要處理 AI/ML 附屬程序(去背、放大、OCR)時,才需要 Python 3.10+。
|
||||
|
||||
## 設定 {#setup}
|
||||
|
||||
```bash
|
||||
git clone https://github.com/snapotter-hq/snapotter.git
|
||||
cd snapotter
|
||||
docker compose -f docker-compose.dev.yml up -d # start Postgres + Redis
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
這會啟動兩個開發伺服器:
|
||||
|
||||
| 服務 | URL | 說明 |
|
||||
|----------|--------------------------|------------------------------------|
|
||||
| 前端 | http://localhost:1349 | Vite 開發伺服器,代理 /api |
|
||||
| 後端 | http://localhost:13490 | Fastify API(透過代理存取) |
|
||||
|
||||
在瀏覽器中開啟 http://localhost:1349。以 `admin` / `admin` 登入。你會在首次登入時被提示變更密碼。
|
||||
|
||||
## 專案結構 {#project-structure}
|
||||
|
||||
```
|
||||
apps/
|
||||
api/ Fastify backend
|
||||
web/ Vite + React frontend
|
||||
docs/ VitePress documentation (this site)
|
||||
packages/
|
||||
shared/ Constants, types, i18n strings
|
||||
image-engine/ Sharp-based image operations
|
||||
media-engine/ FFmpeg spawn + progress parsing
|
||||
doc-engine/ qpdf, LibreOffice, ghostscript wrappers
|
||||
ai/ Python sidecar bridge for ML models
|
||||
tests/
|
||||
unit/ Vitest unit tests
|
||||
integration/ Vitest integration tests (full API)
|
||||
e2e/ Playwright end-to-end specs
|
||||
fixtures/ Small test images
|
||||
```
|
||||
|
||||
## 指令 {#commands}
|
||||
|
||||
```bash
|
||||
pnpm dev # start frontend + backend
|
||||
pnpm build # build all workspaces
|
||||
pnpm typecheck # TypeScript check across monorepo
|
||||
pnpm lint # Biome lint + format check
|
||||
pnpm lint:fix # auto-fix lint + format
|
||||
pnpm test # unit + integration tests
|
||||
pnpm test:unit # unit tests only
|
||||
pnpm test:integration # integration tests only
|
||||
pnpm test:e2e # Playwright e2e tests
|
||||
pnpm test:coverage # tests with coverage report
|
||||
```
|
||||
|
||||
## 程式碼慣例 {#code-conventions}
|
||||
|
||||
- 雙引號、分號、2 格縮排(由 Biome 強制)
|
||||
- 所有工作區皆使用 ES 模組
|
||||
- 供 semantic-release 使用的[慣例式提交](https://www.conventionalcommits.org/)
|
||||
- 所有 API 輸入驗證皆使用 Zod
|
||||
- 不修改 Biome、TypeScript 或編輯器設定檔。修正程式碼,而非 linter。
|
||||
|
||||
## 資料庫 {#database}
|
||||
|
||||
透過 Drizzle ORM(pg-core)使用 PostgreSQL 17。本機開發需要 Postgres 與 Redis 執行中 - 以下列指令啟動它們:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
這會在 5432 埠提供 Postgres,並在 6379 埠提供 Redis。接著產生並套用遷移:
|
||||
|
||||
```bash
|
||||
cd apps/api
|
||||
npx drizzle-kit generate # generate a migration from schema changes
|
||||
npx drizzle-kit migrate # apply pending migrations
|
||||
```
|
||||
|
||||
結構定義於 `apps/api/src/db/schema.ts`。資料表:users、sessions、settings、jobs、apiKeys、pipelines、teams、userFiles、roles、auditLog。
|
||||
|
||||
## 新增工具 {#adding-a-new-tool}
|
||||
|
||||
每個工具都遵循相同的模式。以下是一個最小範例。
|
||||
|
||||
### 1. 後端路由 {#_1-backend-route}
|
||||
|
||||
建立 `apps/api/src/routes/tools/my-tool.ts`:
|
||||
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
intensity: z.number().min(0).max(100).default(50),
|
||||
});
|
||||
|
||||
export function registerMyTool(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "my-tool",
|
||||
settingsSchema,
|
||||
async process(inputBuffer, settings, filename) {
|
||||
// Use sharp or other libraries to process the image
|
||||
const sharp = (await import("sharp")).default;
|
||||
const result = await sharp(inputBuffer)
|
||||
// ... your processing logic
|
||||
.toBuffer();
|
||||
|
||||
return {
|
||||
buffer: result,
|
||||
filename: filename.replace(/\.[^.]+$/, ".png"),
|
||||
contentType: "image/png",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
然後在 `apps/api/src/routes/tools/index.ts` 中註冊它。
|
||||
|
||||
### 2. 前端設定元件 {#_2-frontend-settings-component}
|
||||
|
||||
建立 `apps/web/src/components/tools/my-tool-settings.tsx`:
|
||||
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
export function MyToolSettings() {
|
||||
const { files } = useFileStore();
|
||||
const { processFiles, processing, error, downloadUrl } =
|
||||
useToolProcessor("my-tool");
|
||||
|
||||
const [intensity, setIntensity] = useState(50);
|
||||
|
||||
const handleProcess = () => {
|
||||
processFiles(files, { intensity });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* your controls here */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleProcess}
|
||||
disabled={files.length === 0 || processing}
|
||||
data-testid="my-tool-submit"
|
||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50"
|
||||
>
|
||||
Process
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
然後在位於 `apps/web/src/lib/tool-registry.tsx` 的前端工具登錄表中註冊它:
|
||||
|
||||
```tsx
|
||||
// Add the lazy import
|
||||
const MyToolSettings = lazy(() =>
|
||||
import("@/components/tools/my-tool-settings").then((m) => ({
|
||||
default: m.MyToolSettings,
|
||||
})),
|
||||
);
|
||||
|
||||
// Add to the toolRegistry Map
|
||||
["my-tool", { displayMode: "before-after", Settings: MyToolSettings }],
|
||||
```
|
||||
|
||||
顯示模式:`"side-by-side"`、`"before-after"`、`"live-preview"`、`"no-comparison"`、`"interactive-crop"`、`"interactive-eraser"`、`"no-dropzone"`。
|
||||
|
||||
### 3. i18n 條目 {#_3-i18n-entry}
|
||||
|
||||
新增到 `packages/shared/src/i18n/en.ts`:
|
||||
|
||||
```ts
|
||||
"my-tool": {
|
||||
name: "My Tool",
|
||||
description: "Short description of what this tool does",
|
||||
},
|
||||
```
|
||||
|
||||
### 4. 測試 {#_4-tests}
|
||||
|
||||
為你的動作按鈕新增一個 `data-testid` 屬性(如上所示),這樣 e2e 測試就能可靠地鎖定它。
|
||||
|
||||
## Docker 建置 {#docker-builds}
|
||||
|
||||
在本機建置完整的生產映像檔:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t snapotter:latest .
|
||||
```
|
||||
|
||||
使用 BuildKit 快取掛載以加快重新建置:
|
||||
|
||||
```bash
|
||||
DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile -t snapotter:latest .
|
||||
```
|
||||
|
||||
## 環境變數 {#environment-variables}
|
||||
|
||||
完整清單請見[設定指南](/zh-TW/guide/configuration)。開發時的關鍵變數:
|
||||
|
||||
| 變數 | 預設 | 描述 |
|
||||
|-----------------------------|-----------|------------------------------------------------|
|
||||
| `AUTH_ENABLED` | `true` | 啟用/停用驗證 |
|
||||
| `DEFAULT_USERNAME` | `admin` | 預設管理員使用者名稱 |
|
||||
| `DEFAULT_PASSWORD` | `admin` | 預設管理員密碼 |
|
||||
| `SKIP_MUST_CHANGE_PASSWORD` | `false` | 略過強制變更密碼(僅供 CI/開發) |
|
||||
| `RATE_LIMIT_PER_MIN` | `1000` | 每分鐘的 API 速率限制(0 = 停用) |
|
||||
| `MAX_UPLOAD_SIZE_MB` | `100` | 最大上傳大小(MB)(0 = 無限制) |
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
description: "SnapOtter Docker 映像標籤、GPU 效能基準、版本鎖定,以及 AMD64 與 ARM64 的多平台支援。"
|
||||
i18n_source_hash: 148b3608e11a
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 8b69b4ed0e1b
|
||||
---
|
||||
|
||||
# Docker 映像 {#docker-image}
|
||||
|
||||
SnapOtter 以單一 Docker 映像的形式發佈。單獨執行時,它會在 loopback 介面上啟動內嵌的 PostgreSQL 17 與 Redis(內嵌模式);若用於正式環境,請透過 Compose 讓它與獨立的 PostgreSQL 17 和 Redis 8 容器一同執行。此應用映像可在所有平台上運作。
|
||||
|
||||
## 快速開始 {#quick-start}
|
||||
|
||||
```bash
|
||||
docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data snapotter/snapotter:latest
|
||||
```
|
||||
|
||||
若未設定 `DATABASE_URL`,這會以內嵌模式執行:PostgreSQL 與 Redis 會在容器內的 loopback 上啟動,所有資料都存放在 `SnapOtter-data` 磁碟區下。設定 `DATABASE_URL` 與 `REDIS_URL`(就像 [Compose](#docker-compose) 堆疊那樣)即可改用外部服務。請參閱 [設定](/zh-TW/guide/configuration#embedded-mode)。
|
||||
|
||||
## NVIDIA CUDA 加速 {#nvidia-cuda-acceleration}
|
||||
|
||||
此映像在 amd64 上內含 NVIDIA CUDA 支援。如果你有 NVIDIA GPU 並已安裝 [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html),請加上 `--gpus all`:
|
||||
|
||||
```bash
|
||||
docker run -d --name SnapOtter --gpus all -p 1349:1349 -v SnapOtter-data:/data snapotter/snapotter:latest
|
||||
```
|
||||
|
||||
此映像會在執行階段自動偵測 CUDA。若沒有 `--gpus all`,或當 CUDA 無法使用時,AI 工具會在 CPU 上執行。兩種情況都是同一個映像。
|
||||
|
||||
目前 SnapOtter 的 AI 推論尚不支援透過 VA-API、Quick Sync 或 OpenCL 進行 Intel/AMD iGPU 加速。將 `/dev/dri` 對應進容器可以公開繪圖裝置,但除非 CUDA 可用,否則 AI 執行環境仍會使用 CPU。
|
||||
|
||||
### 效能基準 {#benchmarks}
|
||||
|
||||
在 NVIDIA RTX 4070(12 GB VRAM)上使用一張 572x1024 的 JPEG 人像測試。
|
||||
|
||||
#### 暖啟動效能 {#warm-performance}
|
||||
|
||||
| 工具 | CPU | GPU | 加速倍率 |
|
||||
|------|-----|-----|---------|
|
||||
| 背景移除(u2net) | 2,415ms | 879ms | 2.7x |
|
||||
| 背景移除(isnet) | 2,457ms | 1,137ms | 2.2x |
|
||||
| 放大 2x | 350ms | 309ms | 1.1x |
|
||||
| 放大 4x | 910ms | 310ms | 2.9x |
|
||||
| OCR(PaddleOCR) | 137ms | 94ms | 1.5x |
|
||||
| 臉部模糊 | 139ms | 122ms | 1.1x |
|
||||
|
||||
#### 冷啟動(容器啟動後的第一次請求) {#cold-start-first-request-after-container-start}
|
||||
|
||||
| 工具 | CPU | GPU | 加速倍率 |
|
||||
|------|-----|-----|---------|
|
||||
| 背景移除 | 22,286ms | 4,792ms | 4.7x |
|
||||
| 放大 2x | 3,957ms | 2,318ms | 1.7x |
|
||||
| OCR(PaddleOCR) | 1,469ms | 1,090ms | 1.3x |
|
||||
|
||||
### CUDA 健康檢查 {#cuda-health-check}
|
||||
|
||||
在第一次 AI 請求之後,管理員健康檢查端點會回報 CUDA GPU 狀態:
|
||||
|
||||
```
|
||||
GET /api/v1/admin/health
|
||||
{"ai": {"gpu": true}}
|
||||
```
|
||||
|
||||
## Docker Compose {#docker-compose}
|
||||
|
||||
完整的 Compose 堆疊包含應用程式、PostgreSQL 17 與 Redis 8。完整的 `docker-compose.yml` 請參閱 [部署](/zh-TW/guide/deployment)。一個最精簡的範例:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest
|
||||
ports:
|
||||
- "1349:1349"
|
||||
volumes:
|
||||
- SnapOtter-data:/data
|
||||
- SnapOtter-workspace:/tmp/workspace
|
||||
environment:
|
||||
- DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_USER: snapotter
|
||||
POSTGRES_PASSWORD: snapotter
|
||||
POSTGRES_DB: snapotter
|
||||
volumes:
|
||||
- SnapOtter-pgdata:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U snapotter"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- SnapOtter-redisdata:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
volumes:
|
||||
SnapOtter-data:
|
||||
SnapOtter-workspace:
|
||||
SnapOtter-pgdata:
|
||||
SnapOtter-redisdata:
|
||||
```
|
||||
|
||||
若要透過 Docker Compose 進行 NVIDIA CUDA 加速,請將 deploy 區段加入 SnapOtter 服務:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
```
|
||||
|
||||
## 版本鎖定 {#version-pinning}
|
||||
|
||||
| 標籤 | 說明 |
|
||||
|-----|------------|
|
||||
| `latest` | 最新版本 |
|
||||
| `1.11.0` | 明確版本 |
|
||||
| `1.11` | 1.11.x 中的最新修補版 |
|
||||
| `1` | 1.x 中的最新次要版 |
|
||||
|
||||
## 平台 {#platforms}
|
||||
|
||||
| 架構 | GPU 支援 | 備註 |
|
||||
|---|---|---|
|
||||
| linux/amd64 | NVIDIA CUDA | AI 工具的完整 CUDA 加速 |
|
||||
| linux/arm64 | 僅 CPU | Raspberry Pi 4/5、透過 Docker Desktop 的 Apple Silicon |
|
||||
|
||||
## 從舊標籤遷移 {#migration-from-previous-tags}
|
||||
|
||||
如果你之前使用 `:cuda` 標籤,請改用 `:latest` 並保留 `--gpus all`。相同的 GPU 支援,統一的映像。
|
||||
|
||||
你的資料與設定會保留在磁碟區中。
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
description: "用一道 Docker 指令安裝 SnapOtter。包含 Docker Compose 設定、從原始碼建置,以及完整功能總覽。"
|
||||
i18n_source_hash: 4536d4558b8e
|
||||
i18n_provenance: machine
|
||||
i18n_output_hash: 4e12779bd211
|
||||
---
|
||||
|
||||
# 快速上手 {#getting-started}
|
||||
|
||||
::: tip 安裝前先試用
|
||||
在 [demo.snapotter.com](https://demo.snapotter.com) 探索完整 UI,無需註冊或安裝。
|
||||
:::
|
||||
|
||||
## 快速開始 {#quick-start}
|
||||
|
||||
```bash
|
||||
docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data snapotter/snapotter:latest
|
||||
```
|
||||
|
||||
這個單一容器會執行它所需的一切:在未設定 `DATABASE_URL` 的情況下,它會在 loopback 介面上啟動自己的 PostgreSQL 和 Redis(嵌入模式),並將所有資料保存在 `SnapOtter-data` 磁碟區中。這是試用 SnapOtter 或在家用實驗室自我託管的最快方式。就正式環境而言,請執行下方的 [Docker Compose](#docker-compose) 堆疊,它會將 PostgreSQL 和 Redis 保留在各自的容器中。嵌入模式以 root(預設值)執行,並在你設定 `DATABASE_URL` 後自動關閉。
|
||||
|
||||
首次登入時會要求你變更密碼。
|
||||
|
||||
::: tip 匿名產品分析
|
||||
SnapOtter 預設包含匿名產品分析。若要關閉它,請開啟 **Settings → System → Privacy** 並關閉 **Anonymous Product Analytics**。它會立即對整個執行個體停止。
|
||||
|
||||
你也可以設定環境變數 `SNAPOTTER_TELEMETRY=0`(`false` 和 `off` 也適用)以停用執行個體的所有遙測,無需重新建置。
|
||||
|
||||
錯誤監控由 [Sentry](https://sentry.io) 提供,它透過開源計畫贊助 SnapOtter。
|
||||
|
||||
關於所收集內容的詳細資訊,請參閱 [SnapOtter 收集的內容](/zh-TW/guide/telemetry)。
|
||||
:::
|
||||
|
||||
::: tip NVIDIA CUDA 加速
|
||||
加上 `--gpus all` 以取得 NVIDIA CUDA 加速的去背、放大、OCR、臉部強化與修復:
|
||||
|
||||
```bash
|
||||
docker run -d --name SnapOtter -p 1349:1349 --gpus all -v SnapOtter-data:/data snapotter/snapotter:latest
|
||||
```
|
||||
|
||||
需要 [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)。當 CUDA 不可用時會自動回退至 CPU。目前不支援透過 VA-API、Quick Sync 或 OpenCL 進行 Intel/AMD iGPU 加速的 AI 推論。效能測試請參閱 [Docker Tags](/zh-TW/guide/docker-tags)。
|
||||
:::
|
||||
|
||||
::: details 也在 GHCR 上
|
||||
```bash
|
||||
docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data ghcr.io/snapotter-hq/snapotter:latest
|
||||
```
|
||||
|
||||
兩個登錄檔在每次發行時都會發布相同的映像檔。
|
||||
:::
|
||||
|
||||
## Docker Compose {#docker-compose}
|
||||
|
||||
```yaml
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest # or ghcr.io/snapotter-hq/snapotter:latest
|
||||
ports:
|
||||
- "1349:1349"
|
||||
volumes:
|
||||
- SnapOtter-data:/data
|
||||
environment:
|
||||
- AUTH_ENABLED=true
|
||||
- DEFAULT_USERNAME=admin
|
||||
- DEFAULT_PASSWORD=admin
|
||||
- DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_USER: snapotter
|
||||
POSTGRES_PASSWORD: snapotter
|
||||
POSTGRES_DB: snapotter
|
||||
volumes:
|
||||
- SnapOtter-pgdata:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U snapotter"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- SnapOtter-redisdata:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
volumes:
|
||||
SnapOtter-data:
|
||||
SnapOtter-pgdata:
|
||||
SnapOtter-redisdata:
|
||||
```
|
||||
|
||||
關於所有環境變數,請參閱 [Configuration](/zh-TW/guide/configuration)。
|
||||
|
||||
## 從原始碼建置 {#build-from-source}
|
||||
|
||||
**先決條件:** Node.js 22+、pnpm 9+、Docker(用於 Postgres + Redis)、Python 3.10+(用於 AI 功能)、Git。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/snapotter-hq/SnapOtter.git
|
||||
cd SnapOtter
|
||||
docker compose -f docker-compose.dev.yml up -d # start Postgres + Redis
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- 前端:[http://localhost:1349](http://localhost:1349)
|
||||
- 後端:[http://localhost:13490](http://localhost:13490)
|
||||
|
||||
## 你可以做什麼 {#what-you-can-do}
|
||||
|
||||
### 檔案處理(200+ 工具) {#file-processing-200-tools}
|
||||
|
||||
| 模態 | 數量 | 範例工具 |
|
||||
|----------|-------|---------------|
|
||||
| **影像** | 105 | 調整大小、裁切、壓縮、轉換、去背、放大、OCR、浮水印、拼貼、上色、GIF 工具、格式預設 |
|
||||
| **影片** | 57 | 修剪、裁切、壓縮、轉換、合併、擷取音訊、自動字幕、影片轉 GIF、調整大小、穩定化、格式預設 |
|
||||
| **音訊** | 27 | 修剪、合併、轉換、正規化、雜訊抑制、轉錄、音高變換、淡入淡出、鈴聲製作、格式預設 |
|
||||
| **PDF / 文件** | 42 | 合併、分割、壓縮、OCR、浮水印、遮蔽、Word 轉 PDF、Excel 轉 PDF、旋轉、保護、修復 |
|
||||
| **檔案** | 10 | CSV 轉 JSON、JSON 轉 XML、合併 CSV、分割 CSV、建立 ZIP、解壓縮 ZIP、圖表製作、YAML/JSON |
|
||||
|
||||
### 管線 {#pipelines}
|
||||
|
||||
將工具串連成多步驟工作流程,並套用到一張影像或整個批次:
|
||||
|
||||
1. 在側邊欄開啟 **Pipelines**。
|
||||
2. 新增步驟(任何工具、任何設定)。
|
||||
3. 對單一檔案執行,或一次對整個批次執行。
|
||||
4. 儲存管線以供日後重用。
|
||||
|
||||
管線預設允許 20 個步驟。設定 `MAX_PIPELINE_STEPS=0` 可讓限制變為無限制。
|
||||
|
||||
### 檔案庫 {#file-library}
|
||||
|
||||
你處理的每個檔案都能儲存到你的 **Files** 檔案庫。SnapOtter 會追蹤完整的版本歷史,讓你能從原始上傳到最終輸出追溯每一個處理步驟。
|
||||
|
||||
儲存是明確的:你儲存到檔案庫的結果會保留,直到你刪除它們;而你處理後未儲存的結果會在 72 小時後自動清除(可透過 `FILE_MAX_AGE_HOURS` 設定)。
|
||||
|
||||
### REST API 與 API 金鑰 {#rest-api-api-keys}
|
||||
|
||||
每個工具都可透過 HTTP 存取:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:1349/api/v1/tools/image/resize \
|
||||
-H "Authorization: Bearer si_<your-api-key>" \
|
||||
-F "file=@photo.jpg" \
|
||||
-F 'settings={"width":800,"height":600,"fit":"cover"}'
|
||||
```
|
||||
|
||||
在 **Settings → API Keys** 底下產生 API 金鑰。所有端點請參閱 [REST API 參考](/zh-TW/api/rest),或造訪 [http://localhost:1349/api/docs](http://localhost:1349/api/docs) 以取得互動式參考。
|
||||
|
||||
### 多使用者與團隊 {#multi-user-teams}
|
||||
|
||||
啟用多位使用者並搭配以角色為基礎的存取控制:
|
||||
|
||||
- **管理員**:完整存取 — 管理使用者、團隊、設定,以及所有檔案/管線/API 金鑰
|
||||
- **使用者**:使用工具、管理自己的檔案/管線/API 金鑰
|
||||
|
||||
在 **Settings → Teams** 底下建立團隊以將使用者分組。
|
||||
|
||||
設定 `AUTH_ENABLED=true`(或 `false` 用於單一使用者/自用而不需登入)。
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
description: "使用 OpenID Connect 設定單一登入。針對 Keycloak、Authentik、Google 及其他 OIDC 供應商的逐步指南。"
|
||||
i18n_source_hash: 4296343b3cc5
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 0f8707e9765e
|
||||
---
|
||||
|
||||
# OIDC / 單一登入 {#oidc-single-sign-on}
|
||||
|
||||
SnapOtter 支援 OpenID Connect(OIDC)進行單一登入。使用者可以透過外部身分供應商(例如 Keycloak、Authentik 或 Google)登入,取代本機使用者名稱/密碼驗證(或與之並用)。
|
||||
|
||||
::: tip 另請參閱
|
||||
[SAML SSO](/zh-TW/guide/saml) | [SCIM 佈建](/zh-TW/guide/scim) | [使用者、角色與權限](/zh-TW/guide/users-roles)
|
||||
:::
|
||||
|
||||
## 快速開始 {#quick-start}
|
||||
|
||||
將這些環境變數加入你的 `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest
|
||||
environment:
|
||||
EXTERNAL_URL: "https://photos.example.com"
|
||||
OIDC_ENABLED: "true"
|
||||
OIDC_ISSUER_URL: "https://auth.example.com/realms/myrealm"
|
||||
OIDC_CLIENT_ID: "snapotter"
|
||||
OIDC_CLIENT_SECRET: "your-secret-here"
|
||||
```
|
||||
|
||||
你的供應商所使用的重新導向 URI 一律為:
|
||||
|
||||
```
|
||||
${EXTERNAL_URL}/api/auth/oidc/callback
|
||||
```
|
||||
|
||||
舉例來說,如果 `EXTERNAL_URL` 是 `https://photos.example.com`,請將供應商的重新導向 URI 設定為 `https://photos.example.com/api/auth/oidc/callback`。
|
||||
|
||||
## 設定參考 {#configuration-reference}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `OIDC_ENABLED` | `false` | 啟用 OIDC 登入。登入頁面會出現「使用 SSO 登入」按鈕。 |
|
||||
| `OIDC_ISSUER_URL` | | 供應商的簽發者(issuer)URL。必須支援 OIDC Discovery(`/.well-known/openid-configuration`)。 |
|
||||
| `OIDC_CLIENT_ID` | | 向你的供應商註冊的 OAuth 用戶端 ID。 |
|
||||
| `OIDC_CLIENT_SECRET` | | OAuth 用戶端密鑰。 |
|
||||
| `OIDC_SCOPES` | `openid profile email` | 以空格分隔的要求範圍(scope)清單。 |
|
||||
| `OIDC_AUTO_CREATE_USERS` | `true` | 在首次 OIDC 登入時自動建立本機使用者帳號。 |
|
||||
| `OIDC_DEFAULT_ROLE` | `user` | 指派給自動建立的 OIDC 使用者的角色。可為 `admin`、`editor` 或 `user` 其中之一。 |
|
||||
| `OIDC_AUTO_LINK_USERS` | `false` | 若電子郵件地址相符,將 OIDC 身分連結到現有的本機使用者。 |
|
||||
| `OIDC_PROVIDER_NAME` | | 顯示在登入按鈕上的顯示名稱(例如「Keycloak」、「Google」)。若留空,按鈕會顯示「SSO」。 |
|
||||
| `OIDC_CLOCK_TOLERANCE` | `30` | 用於權杖驗證的時鐘偏差容忍度(秒)。 |
|
||||
| `OIDC_USERNAME_CLAIM` | `preferred_username` | 用作新帳號使用者名稱的 ID 權杖宣告(claim)。 |
|
||||
| `EXTERNAL_URL` | | SnapOtter 可被存取的公開 URL。OIDC 需要此值以建立正確的重新導向 URI。 |
|
||||
| `COOKIE_SECRET` | 自動產生 | 用於簽署工作階段 cookie 的密鑰。執行多個複本時請明確設定此值。 |
|
||||
|
||||
## 供應商指南 {#provider-guides}
|
||||
|
||||
### Keycloak {#keycloak}
|
||||
|
||||
1. 建立新的 realm(或使用現有的)。
|
||||
2. 前往 **Clients** 並建立新的用戶端:
|
||||
- **Client ID**:`snapotter`
|
||||
- **Client authentication**:開啟(confidential)
|
||||
- **Authentication flow**:Standard flow(Authorization Code)
|
||||
3. 在該用戶端的 **Settings** 分頁下,將 **Valid redirect URIs** 設為你的回呼 URL(例如 `https://photos.example.com/api/auth/oidc/callback`)。
|
||||
4. 從 **Credentials** 分頁複製 **Client secret**。
|
||||
5. 將 `OIDC_ISSUER_URL` 設為 `https://keycloak.example.com/realms/your-realm`。
|
||||
|
||||
### Authentik {#authentik}
|
||||
|
||||
1. 在管理介面中,前往 **Applications > Providers** 並建立新的 **OAuth2/OpenID Provider**。
|
||||
- **Client type**:Confidential
|
||||
- **Redirect URIs**:你的回呼 URL
|
||||
- **Signing key**:選擇現有金鑰或建立一個
|
||||
2. 建立一個 **Application** 並將其連結到該供應商。
|
||||
3. 從供應商設定中複製 **Client ID** 與 **Client Secret**。
|
||||
4. 將 `OIDC_ISSUER_URL` 設為 `https://authentik.example.com/application/o/snapotter/`(結尾的斜線很重要)。
|
||||
|
||||
### Google {#google}
|
||||
|
||||
1. 前往 [Google Cloud Console](https://console.cloud.google.com/)。
|
||||
2. 建立專案(或選擇現有的)。
|
||||
3. 導覽至 **APIs & Services > OAuth consent screen** 並進行設定。
|
||||
4. 前往 **APIs & Services > Credentials** 並建立 **OAuth 2.0 Client ID**:
|
||||
- **Application type**:Web application
|
||||
- **Authorized redirect URIs**:你的回呼 URL
|
||||
5. 複製 **Client ID** 與 **Client secret**。
|
||||
6. 將 `OIDC_ISSUER_URL` 設為 `https://accounts.google.com`。
|
||||
7. 將 `OIDC_USERNAME_CLAIM` 設為 `email`(Google 不提供 `preferred_username`)。
|
||||
|
||||
## 使用者佈建 {#user-provisioning}
|
||||
|
||||
### 自動建立 {#auto-create}
|
||||
|
||||
當 `OIDC_AUTO_CREATE_USERS` 為 `true`(預設)時,會在有人首次透過 OIDC 登入時建立本機使用者帳號。使用者名稱取自由 `OIDC_USERNAME_CLAIM` 指定的宣告,角色則設為 `OIDC_DEFAULT_ROLE`。
|
||||
|
||||
若發生使用者名稱衝突,會附加一個數字後綴(例如 `jane` 會變成 `jane_2`)。
|
||||
|
||||
### 自動連結 {#auto-link}
|
||||
|
||||
當 `OIDC_AUTO_LINK_USERS` 為 `true` 時,若電子郵件地址相符,SnapOtter 會將 OIDC 身分連結到現有的本機帳號。當你已預先建立使用者帳號,並希望他們在不遺失資料的情況下開始使用 SSO 時,這很有用。
|
||||
|
||||
::: warning
|
||||
只有在你信任你的 OIDC 供應商會驗證電子郵件地址時,才啟用自動連結。未經驗證的電子郵件可能讓某人接管其他使用者的帳號。
|
||||
:::
|
||||
|
||||
### 停用本機登入 {#disabling-local-login}
|
||||
|
||||
OIDC 不會停用本機使用者名稱/密碼登入。兩種方法都仍可使用。若 OIDC 供應商無法連線,管理員仍可使用本機憑證登入。
|
||||
|
||||
## 自簽憑證 {#self-signed-certificates}
|
||||
|
||||
如果你的 OIDC 供應商使用自簽或私有 CA 憑證,請將 CA 套件掛載進容器,並讓 `NODE_EXTRA_CA_CERTS` 指向它:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest
|
||||
volumes:
|
||||
- ./my-ca.pem:/etc/ssl/certs/custom-ca.pem:ro
|
||||
environment:
|
||||
NODE_EXTRA_CA_CERTS: /etc/ssl/certs/custom-ca.pem
|
||||
OIDC_ENABLED: "true"
|
||||
OIDC_ISSUER_URL: "https://auth.internal.example.com/realms/myrealm"
|
||||
OIDC_CLIENT_ID: "snapotter"
|
||||
OIDC_CLIENT_SECRET: "your-secret-here"
|
||||
```
|
||||
|
||||
::: danger
|
||||
請勿設定 `NODE_TLS_REJECT_UNAUTHORIZED=0`。這會停用所有 TLS 驗證,是一項安全風險。
|
||||
:::
|
||||
|
||||
## 疑難排解 {#troubleshooting}
|
||||
|
||||
### 重新導向 URI 不相符 {#redirect-uri-mismatch}
|
||||
|
||||
最常見的錯誤。請檢查你的供應商所預期的內容與 SnapOtter 所送出的內容之間是否有以下差異:
|
||||
|
||||
- `http` 對 `https`,配置方式必須完全相符
|
||||
- 結尾斜線,某些供應商對此很嚴格
|
||||
- 連接埠號碼,若非標準連接埠請一併納入
|
||||
- 路徑,必須是 `/api/auth/oidc/callback`
|
||||
|
||||
請再次確認 `EXTERNAL_URL`。它必須與使用者在瀏覽器中輸入的 URL 相符。
|
||||
|
||||
### UNABLE_TO_VERIFY_LEAF_SIGNATURE {#unable-to-verify-leaf-signature}
|
||||
|
||||
OIDC 供應商所使用的憑證不受 Node.js 信任。請參閱上方的 [自簽憑證](#self-signed-certificates)。
|
||||
|
||||
### 時鐘偏差錯誤 {#clock-skew-errors}
|
||||
|
||||
如果你的伺服器時鐘與 OIDC 供應商時鐘不同步,權杖驗證可能會失敗。請提高 `OIDC_CLOCK_TOLERANCE`(預設為 30 秒)。更好的做法是在兩台機器上都執行 NTP。
|
||||
|
||||
### 「OIDC provider unreachable」 {#oidc-provider-unreachable}
|
||||
|
||||
SnapOtter 會在啟動時以及登入期間擷取供應商的探索文件。請檢查:
|
||||
|
||||
- 從 Docker 容器內部進行的 DNS 解析(`docker exec snapotter nslookup auth.example.com`)
|
||||
- 容器與供應商之間的防火牆規則
|
||||
- `OIDC_ISSUER_URL` 值,它必須能從伺服器連線,而不僅僅是從你的瀏覽器
|
||||
|
||||
### 缺少宣告 {#missing-claims}
|
||||
|
||||
如果登入後使用者名稱或電子郵件為空,你的供應商可能沒有回傳預期的宣告。請驗證:
|
||||
|
||||
- 在 `OIDC_SCOPES` 中設定的範圍包含 `profile` 與 `email`
|
||||
- 供應商已設定為在 ID 權杖中包含由 `OIDC_USERNAME_CLAIM` 指定的宣告
|
||||
- 某些供應商需要明確的對應(mapper)/範圍設定才能釋出宣告
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
description: "為 SnapOtter 設定 SAML 2.0 單一登入。針對 Okta、Azure AD / Entra ID、Google Workspace 及其他 SAML 身分供應商的逐步指南。"
|
||||
i18n_source_hash: 33dfb8b02a22
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 8816672cd13a
|
||||
---
|
||||
|
||||
# SAML SSO {#saml-sso}
|
||||
|
||||
SnapOtter 支援 SAML 2.0 進行單一登入。使用者可以透過外部身分供應商(Okta、Azure AD / Entra ID、Google Workspace,或任何標準 SAML 2.0 IdP)登入,取代本機使用者名稱/密碼驗證。
|
||||
|
||||
::: tip 企業版功能
|
||||
SAML SSO 需要具備 `saml_sso` 功能的 **team** 或 **enterprise** 授權。若設定了 `SAML_ENABLED=true` 卻沒有有效授權,SAML 路由會被靜默略過並記錄一則警告。
|
||||
:::
|
||||
|
||||
## 先決條件 {#prerequisites}
|
||||
|
||||
- 一個可透過公開 URL 存取的執行中 SnapOtter 執行個體
|
||||
- 將 `EXTERNAL_URL` 設為該公開 URL(例如 `https://photos.example.com`)
|
||||
- 一個具備 `saml_sso` 功能的 team 或 enterprise 授權金鑰
|
||||
- 你 SAML 身分供應商的管理員存取權
|
||||
|
||||
## 快速開始 {#quick-start}
|
||||
|
||||
將這些環境變數加入你的 `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
snapotter:
|
||||
image: snapotter/snapotter:latest
|
||||
environment:
|
||||
EXTERNAL_URL: "https://photos.example.com"
|
||||
SNAPOTTER_LICENSE_KEY: "your-license-key"
|
||||
SAML_ENABLED: "true"
|
||||
SAML_IDP_SSO_URL: "https://idp.example.com/sso/saml"
|
||||
SAML_IDP_CERTIFICATE: |
|
||||
MIICpDCCAYwCCQDU+pQ4pHgSpDANBgkqhkiG9w0BAQsFADAUMRIw
|
||||
...your IdP's signing certificate in PEM format...
|
||||
EAYHKoZIzj0CAQYFK4EEACIDYgAE
|
||||
```
|
||||
|
||||
重新啟動容器。登入頁面會出現「使用 SAML 登入」按鈕(或由 `SAML_PROVIDER_NAME` 設定的標籤)。
|
||||
|
||||
## 設定參考 {#configuration-reference}
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|---|---|---|
|
||||
| `SAML_ENABLED` | `false` | 啟用 SAML 登入。 |
|
||||
| `SAML_IDP_SSO_URL` | | IdP 的 SSO 端點 URL。啟用 SAML 時為**必填**。 |
|
||||
| `SAML_IDP_CERTIFICATE` | | IdP 的 PEM 格式 X.509 簽章憑證(憑證文字本身,而非檔案路徑)。啟用 SAML 時為**必填**。 |
|
||||
| `EXTERNAL_URL` | | SnapOtter 可被存取的公開 URL。啟用 SAML 時為**必填**。 |
|
||||
| `SAML_ENTITY_ID` | `${EXTERNAL_URL}/api/auth/saml/metadata` | 送往 IdP 的 SP Entity ID / Audience URI。 |
|
||||
| `SAML_CALLBACK_URL` | `${EXTERNAL_URL}/api/auth/saml/callback` | Assertion Consumer Service(ACS)URL。 |
|
||||
| `SAML_AUTO_CREATE_USERS` | `true` | 在首次 SAML 登入時自動建立本機使用者帳號。 |
|
||||
| `SAML_AUTO_LINK_USERS` | `false` | 若電子郵件地址相符,將 SAML 身分連結到現有的本機使用者。 |
|
||||
| `SAML_DEFAULT_ROLE` | `user` | 指派給自動建立的 SAML 使用者的角色。可為 `admin`、`editor` 或 `user` 其中之一。 |
|
||||
| `SAML_PROVIDER_NAME` | | 前端 SAML 登入按鈕的顯示標籤(例如「Okta」、「Azure AD」)。若留空,按鈕會顯示「SAML」。 |
|
||||
| `SAML_USERNAME_ATTRIBUTE` | | 用作使用者名稱的 SAML 斷言屬性。若留空,會回退到電子郵件的本機部分,再回退到 NameID。 |
|
||||
| `SAML_EMAIL_ATTRIBUTE` | `email` | 用作使用者電子郵件地址的 SAML 斷言屬性。 |
|
||||
|
||||
若 `SAML_ENABLED=true` 且缺少三個必填變數(`SAML_IDP_SSO_URL`、`SAML_IDP_CERTIFICATE`、`EXTERNAL_URL`)中的任何一個,伺服器會拒絕啟動。
|
||||
|
||||
::: details 安全性注意事項
|
||||
`wantAuthnResponseSigned` 與 `wantAssertionsSigned` 兩者都硬式編碼為 `true`。SnapOtter 會拒絕未簽章或簽章不正確的 SAML 回應。來自受信任 IdP 的斷言會被視為電子郵件已驗證。
|
||||
|
||||
僅支援 SP 發起的登入。SnapOtter 不支援 IdP 發起(未經請求)的登入或單一登出(SLO)。登出 SnapOtter 不會將使用者從 IdP 登出。
|
||||
:::
|
||||
|
||||
## SP 中繼資料與 URL {#sp-metadata-and-urls}
|
||||
|
||||
你的 IdP 需要來自 SnapOtter 的三個值:
|
||||
|
||||
| 欄位 | 值 |
|
||||
|---|---|
|
||||
| **ACS URL**(Assertion Consumer Service) | `${EXTERNAL_URL}/api/auth/saml/callback` |
|
||||
| **Entity ID** / **Audience URI** | `${EXTERNAL_URL}/api/auth/saml/metadata` |
|
||||
| **SP Metadata**(XML) | `GET ${EXTERNAL_URL}/api/auth/saml/metadata` |
|
||||
|
||||
舉例來說,如果 `EXTERNAL_URL` 是 `https://photos.example.com`:
|
||||
|
||||
- ACS URL:`https://photos.example.com/api/auth/saml/callback`
|
||||
- Entity ID:`https://photos.example.com/api/auth/saml/metadata`
|
||||
- 中繼資料端點:`https://photos.example.com/api/auth/saml/metadata`(回傳 XML)
|
||||
|
||||
某些 IdP 可以直接匯入 SP 中繼資料 URL,這會自動填入 ACS URL 與 Entity ID。
|
||||
|
||||
## 供應商設定 {#provider-setup}
|
||||
|
||||
### Okta {#okta}
|
||||
|
||||
1. 在 Okta 管理主控台中,前往 **Applications > Create App Integration**。
|
||||
2. 選擇 **SAML 2.0** 並點按 **Next**。
|
||||
3. 設定名稱(例如「SnapOtter」)並點按 **Next**。
|
||||
4. 設定 SAML 設定:
|
||||
- **Single sign-on URL**:你的 ACS URL(例如 `https://photos.example.com/api/auth/saml/callback`)
|
||||
- **Audience URI (SP Entity ID)**:你的 Entity ID(例如 `https://photos.example.com/api/auth/saml/metadata`)
|
||||
- **Name ID format**:EmailAddress
|
||||
- **Application username**:Email
|
||||
5. 在 **Attribute Statements** 下,新增對應到 `user.email` 的 `email`。
|
||||
6. 點按 **Next**,再點按 **Finish**。
|
||||
7. 前往 **Sign On** 分頁,點按 **View SAML setup instructions**,並複製:
|
||||
- 將 **Identity Provider Single Sign-On URL** 填入 `SAML_IDP_SSO_URL`
|
||||
- 將 **X.509 Certificate** 填入 `SAML_IDP_CERTIFICATE`
|
||||
|
||||
### Azure AD / Entra ID {#azure-ad-entra-id}
|
||||
|
||||
1. 在 Azure 入口網站中,前往 **Microsoft Entra ID > Enterprise applications > New application**。
|
||||
2. 點按 **Create your own application**,命名為「SnapOtter」,並選擇 **Integrate any other application you don't find in the gallery**。
|
||||
3. 前往 **Single sign-on > SAML** 並在 **Basic SAML Configuration** 區段點按 **Edit**:
|
||||
- **Identifier (Entity ID)**:你的 Entity ID(例如 `https://photos.example.com/api/auth/saml/metadata`)
|
||||
- **Reply URL (ACS URL)**:你的 ACS URL(例如 `https://photos.example.com/api/auth/saml/callback`)
|
||||
4. 在 **SAML Certificates** 下,下載 **Certificate (Base64)**。
|
||||
5. 在 **Set up SnapOtter** 下,複製 **Login URL**。
|
||||
6. 將 `SAML_IDP_SSO_URL` 設為 Login URL,並將 `SAML_IDP_CERTIFICATE` 設為下載的憑證內容。
|
||||
7. 在 **Users and groups** 下將使用者或群組指派給此應用程式。
|
||||
|
||||
### Google Workspace {#google-workspace}
|
||||
|
||||
1. 在 Google 管理主控台中,前往 **Apps > Web and mobile apps > Add app > Add custom SAML app**。
|
||||
2. 將應用程式命名為「SnapOtter」並點按 **Continue**。
|
||||
3. 在 **Google Identity Provider details** 頁面,複製 **SSO URL** 並下載 **Certificate**。點按 **Continue**。
|
||||
4. 設定 Service Provider 詳細資訊:
|
||||
- **ACS URL**:你的 ACS URL(例如 `https://photos.example.com/api/auth/saml/callback`)
|
||||
- **Entity ID**:你的 Entity ID(例如 `https://photos.example.com/api/auth/saml/metadata`)
|
||||
- **Name ID format**:EMAIL
|
||||
- **Name ID**:Basic Information > Primary email
|
||||
5. 點按 **Continue**,再點按 **Finish**。
|
||||
6. 為你的組織單位將應用程式**開啟(ON)**。
|
||||
7. 將 `SAML_IDP_SSO_URL` 設為步驟 3 的 SSO URL,並將 `SAML_IDP_CERTIFICATE` 設為下載的憑證內容。
|
||||
|
||||
### 通用 SAML 2.0 IdP {#generic-saml-2-0-idp}
|
||||
|
||||
對於任何符合 SAML 2.0 規範的身分供應商:
|
||||
|
||||
1. 在你的 IdP 中建立新的 SAML 應用程式/服務供應商。
|
||||
2. 將 **ACS URL** 設為 `${EXTERNAL_URL}/api/auth/saml/callback`。
|
||||
3. 將 **Entity ID** / **Audience** 設為 `${EXTERNAL_URL}/api/auth/saml/metadata`。
|
||||
4. 設定 IdP 在名為 `email` 的屬性中送出使用者的電子郵件(或設定 `SAML_EMAIL_ATTRIBUTE` 以符合你 IdP 的屬性名稱)。
|
||||
5. 將 **IdP SSO URL** 與 **signing certificate** 複製到 `SAML_IDP_SSO_URL` 與 `SAML_IDP_CERTIFICATE`。
|
||||
|
||||
## 使用者佈建 {#user-provisioning}
|
||||
|
||||
### 自動建立 {#auto-create}
|
||||
|
||||
當 `SAML_AUTO_CREATE_USERS` 為 `true`(預設)時,會在有人首次透過 SAML 登入時建立本機使用者帳號。角色會設為 `SAML_DEFAULT_ROLE`。
|
||||
|
||||
使用者名稱按以下順序衍生:
|
||||
|
||||
1. 由 `SAML_USERNAME_ATTRIBUTE` 指定的斷言屬性值(若已設定且存在)
|
||||
2. 電子郵件地址的本機部分(`@` 之前的所有內容)
|
||||
3. SAML NameID
|
||||
|
||||
若發生使用者名稱衝突,會附加一個數字後綴(例如 `jane` 會變成 `jane_2`)。
|
||||
|
||||
### 自動連結 {#auto-link}
|
||||
|
||||
當 `SAML_AUTO_LINK_USERS` 為 `true` 時,若電子郵件地址相符,SnapOtter 會將 SAML 身分連結到現有的本機帳號。當你已預先建立使用者帳號,並希望他們在不遺失資料的情況下開始使用 SSO 時,這很有用。
|
||||
|
||||
::: warning
|
||||
只有在你信任你的 SAML IdP 會驗證電子郵件地址時,才啟用自動連結。來自設定錯誤的 IdP 的未經驗證電子郵件,可能讓某人接管其他使用者的帳號。
|
||||
:::
|
||||
|
||||
### 屬性對應 {#attribute-mapping}
|
||||
|
||||
| SnapOtter 欄位 | 來源 | 設定 |
|
||||
|---|---|---|
|
||||
| Email | 斷言屬性 | `SAML_EMAIL_ATTRIBUTE`(預設:`email`) |
|
||||
| Username | 斷言屬性、電子郵件或 NameID | `SAML_USERNAME_ATTRIBUTE`(請參閱上方的衍生順序) |
|
||||
| External ID | NameID | 一律為 SAML NameID,不可設定 |
|
||||
|
||||
## SSO 強制 {#sso-enforcement}
|
||||
|
||||
如果你想要求所有使用者透過 SAML(或 OIDC)登入,並封鎖本機密碼登入,請啟用 SSO 強制:
|
||||
|
||||
1. 確保 `sso_enforcement` 企業版功能已獲授權(team 與 enterprise 方案提供)。
|
||||
2. 在 **Admin Settings > Security** 中,開啟 **SSO Enforcement**。
|
||||
3. 設定一個 **break-glass 使用者名稱**:這是唯一在 IdP 無法連線時仍可用密碼登入以進行緊急存取的本機帳號。
|
||||
|
||||
當 SSO 強制啟用時,任何本機登入嘗試(break-glass 使用者除外)都會回傳 403 錯誤,訊息為「Local password login is disabled. Please use SSO.」
|
||||
|
||||
::: tip
|
||||
在啟用 SSO 強制之前,一律要先設定 break-glass 使用者名稱。若沒有它,當你的 IdP 停機時,你可能會被鎖在 SnapOtter 之外。
|
||||
:::
|
||||
|
||||
## 將 SAML 與 OIDC 並用 {#using-saml-alongside-oidc}
|
||||
|
||||
SAML 與 OIDC 可以同時啟用。當兩者都啟用時,登入頁面會為每個供應商顯示各自的按鈕(由 `SAML_PROVIDER_NAME` 與 `OIDC_PROVIDER_NAME` 標示)。使用者可以用任一方法登入。
|
||||
|
||||
兩個供應商各自獨立共用相同的自動建立、自動連結與 SSO 強制設定:每個都有自己的 `*_AUTO_CREATE_USERS`、`*_AUTO_LINK_USERS` 與 `*_DEFAULT_ROLE` 變數。
|
||||
|
||||
## 疑難排解 {#troubleshooting}
|
||||
|
||||
### 斷言驗證失敗 {#assertion-validation-failed}
|
||||
|
||||
SAML 回應簽章或斷言簽章無法驗證。請檢查:
|
||||
|
||||
- `SAML_IDP_CERTIFICATE` 中的憑證與你 IdP 中目前的簽章憑證相符(憑證會輪替,所以請檢查是否過期)
|
||||
- 憑證為 PEM 格式(以 `-----BEGIN CERTIFICATE-----` 開頭)
|
||||
- 憑證為完整文字,而非檔案路徑
|
||||
- 你 IdP 中設定的 ACS URL 與 Entity ID 與 SnapOtter 的值完全相符(配置方式、主機、連接埠、路徑)
|
||||
|
||||
### 缺少屬性 {#missing-attributes}
|
||||
|
||||
如果登入後使用者名稱或電子郵件為空,你的 IdP 可能沒有送出預期的屬性。請檢查:
|
||||
|
||||
- 你的 IdP 已設定為釋出 `email` 屬性(或 `SAML_EMAIL_ATTRIBUTE` 所設定的任何值)
|
||||
- 若使用 `SAML_USERNAME_ATTRIBUTE`,請驗證該屬性已包含在斷言中
|
||||
- 某些 IdP 需要明確的屬性對應設定,才會釋出宣告
|
||||
|
||||
### 時鐘偏差 {#clock-skew}
|
||||
|
||||
SAML 斷言包含時間戳記條件(`NotBefore`、`NotOnOrAfter`)。如果你的伺服器時鐘與 IdP 時鐘不同步,斷言驗證會失敗。在兩台機器上都執行 NTP 以保持時鐘一致。
|
||||
|
||||
### 「SAML is enabled via env but saml_sso enterprise feature is not licensed」 {#saml-is-enabled-via-env-but-saml-sso-enterprise-feature-is-not-licensed}
|
||||
|
||||
當 `SAML_ENABLED=true` 但授權不包含 `saml_sso` 功能時,此警告會出現在伺服器記錄中。請驗證你的授權金鑰與方案。`saml_sso` 功能在 team 與 enterprise 方案提供。
|
||||
|
||||
### 登入重新導向後帶著錯誤返回 {#login-redirects-back-with-error}
|
||||
|
||||
如果點按 SAML 登入按鈕後帶著錯誤重新導向回登入頁面,請檢查伺服器記錄以取得詳細資訊。常見原因:
|
||||
|
||||
- 從伺服器無法連線到 IdP SSO URL
|
||||
- IdP 拒絕了驗證請求(請檢查 IdP 的稽核記錄)
|
||||
- IdP 回傳了未簽章的回應(SnapOtter 要求回應與斷言兩者都必須簽章)
|
||||
@@ -0,0 +1,298 @@
|
||||
---
|
||||
description: "設定 SCIM 2.0 佈建,將使用者與群組從您的身分提供者同步至 SnapOtter。涵蓋 Okta、Azure AD / Entra ID 以及自訂整合。"
|
||||
i18n_source_hash: bbd50119ec12
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 674e1fe7bfc1
|
||||
---
|
||||
|
||||
# SCIM 佈建 {#scim-provisioning}
|
||||
|
||||
SnapOtter 實作 SCIM 2.0(System for Cross-domain Identity Management)以進行自動化的使用者與群組佈建。您的身分提供者可以自動建立、更新、停用及重新啟用使用者帳號,並同步群組成員資格。
|
||||
|
||||
::: tip 企業版功能
|
||||
SCIM 佈建需要具備 `scim` 功能的 **enterprise** 授權。team 方案無法使用。若未具備此功能,所有 SCIM 端點(探索端點除外)都會回傳 403。
|
||||
:::
|
||||
|
||||
## 先決條件 {#prerequisites}
|
||||
|
||||
- 一個可透過公開網址存取的執行中 SnapOtter 執行個體
|
||||
- 具備 `scim` 功能的企業版授權金鑰
|
||||
- SnapOtter 的管理員存取權(產生或撤銷 SCIM 權杖需要 `users:manage` 權限)
|
||||
- 您身分提供者佈建設定的管理員存取權
|
||||
|
||||
## 快速開始 {#quick-start}
|
||||
|
||||
1. 產生一個 SCIM bearer 權杖:
|
||||
|
||||
```bash
|
||||
curl -X POST https://photos.example.com/api/v1/enterprise/scim/token \
|
||||
-H "Cookie: snapotter-session=YOUR_SESSION" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
回應中包含該權杖。請立即儲存;它無法再次取得。
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "a1b2c3d4e5f6...",
|
||||
"message": "Save this token - it cannot be retrieved again"
|
||||
}
|
||||
```
|
||||
|
||||
2. 在您的身分提供者中,以下列項目設定 SCIM 佈建:
|
||||
- **Base URL**:`https://photos.example.com/api/v1/scim/v2`
|
||||
- **Authentication**:Bearer 權杖(貼上步驟 1 中的權杖)
|
||||
|
||||
## 驗證 {#authentication}
|
||||
|
||||
SCIM 端點使用專屬的 Bearer 權杖,與使用者工作階段及 API 金鑰分開。
|
||||
|
||||
### 產生權杖 {#generating-a-token}
|
||||
|
||||
`POST /api/v1/enterprise/scim/token` 會產生一個新的 SCIM 權杖。此端點需要具備 `users:manage` 權限的有效工作階段。
|
||||
|
||||
該權杖以明文回傳,且僅回傳一次。SnapOtter 只儲存 scrypt 雜湊值。若您遺失權杖,請撤銷它並產生新的權杖。
|
||||
|
||||
同一時間只有一個 SCIM 權杖處於使用中狀態。產生新權杖會取代先前的權杖。
|
||||
|
||||
### 撤銷權杖 {#revoking-a-token}
|
||||
|
||||
`DELETE /api/v1/enterprise/scim/token` 會撤銷目前的 SCIM 權杖。此端點同樣需要 `users:manage`。
|
||||
|
||||
### 速率限制 {#rate-limiting}
|
||||
|
||||
SCIM 端點的速率限制為每個權杖每分鐘 1000 個請求。超過此限制會回傳 HTTP 429。
|
||||
|
||||
## 支援的資源 {#supported-resources}
|
||||
|
||||
| SCIM 資源 | SnapOtter 概念 | 建立 | 讀取 | 更新 | 刪除 |
|
||||
|---|---|---|---|---|---|
|
||||
| User | 使用者帳號 | 是 | 是 | 是 | 軟刪除 |
|
||||
| Group | 團隊 | 是 | 是 | 是 | 是 |
|
||||
|
||||
::: warning
|
||||
SCIM Group 對應到 SnapOtter 的**團隊**,而非角色。SCIM 無法設定使用者的角色。所有透過 SCIM 建立的使用者都會被指派 `user` 角色。若要變更使用者的角色,請使用 SnapOtter 管理員 UI。
|
||||
:::
|
||||
|
||||
## 使用者操作 {#user-operations}
|
||||
|
||||
### 建立使用者 {#create-user}
|
||||
|
||||
`POST /api/v1/scim/v2/Users`
|
||||
|
||||
建立一個新的使用者帳號,其 `authProvider` 設為 `scim` 並具備 `user` 角色。該使用者會被指派到 Default 團隊。若 `active` 為 `false`,則角色會改設為 `disabled`。
|
||||
|
||||
必要屬性:`userName`。選用屬性:`externalId`、`emails`、`active`(預設為 `true`)。
|
||||
|
||||
### 列出並篩選使用者 {#list-and-filter-users}
|
||||
|
||||
`GET /api/v1/scim/v2/Users`
|
||||
|
||||
回傳使用者的分頁清單。支援 `startIndex` 與 `count` 查詢參數(每頁最多 200 筆結果)。
|
||||
|
||||
篩選僅支援 `eq`(等於),適用於下列屬性:
|
||||
|
||||
- `userName eq "jane"`
|
||||
- `externalId eq "ext-12345"`
|
||||
|
||||
其他篩選運算子與屬性會回傳 HTTP 400。
|
||||
|
||||
### 取得使用者 {#get-user}
|
||||
|
||||
`GET /api/v1/scim/v2/Users/:id`
|
||||
|
||||
依 SnapOtter 使用者 ID 回傳單一使用者。
|
||||
|
||||
### 取代使用者 {#replace-user}
|
||||
|
||||
`PUT /api/v1/scim/v2/Users/:id`
|
||||
|
||||
取代使用者的屬性。支援 `userName`、`externalId`、`emails` 與 `active`。使用者名稱變更會檢查衝突(若新使用者名稱已被其他使用者佔用則回傳 409)。
|
||||
|
||||
### 修補使用者 {#patch-user}
|
||||
|
||||
`PATCH /api/v1/scim/v2/Users/:id`
|
||||
|
||||
使用 SCIM PatchOp 進行部分更新。支援的操作:
|
||||
|
||||
| 操作 | 路徑 |
|
||||
|---|---|
|
||||
| `replace` | `active`、`userName`、`externalId`、`emails`、`emails[type eq "work"].value`、`name.formatted`、`displayName` |
|
||||
| `add` | 與 `replace` 相同 |
|
||||
| `remove` | `externalId`、`emails` |
|
||||
|
||||
`name.formatted` 與 `displayName` 路徑為了相容性而被接受,但沒有持久效果(SnapOtter 不會另外儲存顯示名稱)。
|
||||
|
||||
無值的 `replace` 操作(其值為一個不含 `path` 的物件)同樣受支援,鍵為 `userName`、`externalId`、`emails` 與 `active`。
|
||||
|
||||
### 停用使用者(軟刪除) {#deactivate-user-soft-delete}
|
||||
|
||||
`DELETE /api/v1/scim/v2/Users/:id`
|
||||
|
||||
SnapOtter 不會透過 SCIM 硬刪除使用者。相反地,DELETE 會執行軟停用:
|
||||
|
||||
1. 使用者的角色會從其目前值(例如 `editor`)變更為 `disabled:editor`,並保留原本的角色。
|
||||
2. 使用者的密碼會被清除。
|
||||
3. 所有使用中的工作階段都會被撤銷。
|
||||
4. 所有 API 金鑰都會被撤銷。
|
||||
|
||||
該使用者將無法再登入或使用任何 API 金鑰。他們的資料(檔案、歷史記錄)會被保留。
|
||||
|
||||
### 重新啟用使用者 {#reactivate-user}
|
||||
|
||||
若要重新啟用先前已停用的使用者,請以 `active: true` 傳送 `PUT` 或 `PATCH` 請求。SnapOtter 會還原停用前的原始角色(例如 `disabled:editor` 會再次變回 `editor`)。若無法判定原始角色,則會回退至 `user`。
|
||||
|
||||
::: details 範例:透過 PATCH 停用及重新啟用
|
||||
```json
|
||||
// Deactivate
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{ "op": "replace", "path": "active", "value": false }
|
||||
]
|
||||
}
|
||||
|
||||
// Reactivate
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{ "op": "replace", "path": "active", "value": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## 群組操作 {#group-operations}
|
||||
|
||||
SCIM Group 對應到 SnapOtter 團隊。建立群組會建立一個團隊。群組成員資格控制使用者所屬的團隊。
|
||||
|
||||
### 建立群組 {#create-group}
|
||||
|
||||
`POST /api/v1/scim/v2/Groups`
|
||||
|
||||
必要:`displayName`。選用:`members`(`{ value: userId }` 的陣列)。
|
||||
|
||||
### 列出並篩選群組 {#list-and-filter-groups}
|
||||
|
||||
`GET /api/v1/scim/v2/Groups`
|
||||
|
||||
篩選僅支援 `displayName eq "..."`。以 `startIndex` 與 `count` 分頁(每頁最多 200 筆結果)。
|
||||
|
||||
### 取得群組 {#get-group}
|
||||
|
||||
`GET /api/v1/scim/v2/Groups/:id`
|
||||
|
||||
### 取代群組 {#replace-group}
|
||||
|
||||
`PUT /api/v1/scim/v2/Groups/:id`
|
||||
|
||||
取代群組名稱與完整成員清單。不在新清單中的現有成員會被移至 Default 團隊。
|
||||
|
||||
### 修補群組 {#patch-group}
|
||||
|
||||
`PATCH /api/v1/scim/v2/Groups/:id`
|
||||
|
||||
支援下列操作:
|
||||
|
||||
| 操作 | 路徑 | 效果 |
|
||||
|---|---|---|
|
||||
| `add` | `members` | 將使用者加入團隊 |
|
||||
| `remove` | `members[value eq "userId"]` | 將使用者移至 Default 團隊 |
|
||||
| `replace` | `displayName` | 重新命名團隊 |
|
||||
| `replace` | `members` | 取代所有成員(被移除的成員會移至 Default 團隊) |
|
||||
|
||||
### 刪除群組 {#delete-group}
|
||||
|
||||
`DELETE /api/v1/scim/v2/Groups/:id`
|
||||
|
||||
刪除該團隊。被刪除團隊的所有成員都會被移至 Default 團隊。使用者不會被停用或刪除。
|
||||
|
||||
## IdP 設定 {#idp-setup}
|
||||
|
||||
### Okta {#okta}
|
||||
|
||||
1. 在 Okta 管理員主控台中,開啟您的 SnapOtter 應用程式(或建立一個)。
|
||||
2. 前往 **Provisioning** 分頁並點選 **Configure API Integration**。
|
||||
3. 勾選 **Enable API Integration** 並輸入:
|
||||
- **Base URL**:`https://photos.example.com/api/v1/scim/v2`
|
||||
- **API Token**:上方產生的 SCIM bearer 權杖
|
||||
4. 點選 **Test API Credentials**,然後點選 **Save**。
|
||||
5. 在 **Provisioning > To App** 下,啟用:
|
||||
- **Create Users**
|
||||
- **Update User Attributes**
|
||||
- **Deactivate Users**
|
||||
6. 在 **Push Groups** 下,設定要以 SnapOtter 團隊同步的 Okta 群組。
|
||||
|
||||
### Azure AD / Entra ID {#azure-ad-entra-id}
|
||||
|
||||
1. 在 Azure 入口網站中,前往您的 SnapOtter 企業應用程式。
|
||||
2. 前往 **Provisioning** 並將 **Provisioning Mode** 設為 **Automatic**。
|
||||
3. 在 **Admin Credentials** 下,輸入:
|
||||
- **Tenant URL**:`https://photos.example.com/api/v1/scim/v2`
|
||||
- **Secret Token**:上方產生的 SCIM bearer 權杖
|
||||
4. 點選 **Test Connection**,然後點選 **Save**。
|
||||
5. 在 **Mappings** 下,設定使用者與群組屬性對應。預設值通常可正常運作,但請確認 `userName` 依需求對應到 `userPrincipalName` 或 `mail`。
|
||||
6. 將 **Provisioning Status** 設為 **On** 並儲存。
|
||||
|
||||
Azure 會依固定的同步週期(通常每 40 分鐘)佈建使用者與群組。
|
||||
|
||||
## 探索端點 {#discovery-endpoints}
|
||||
|
||||
下列三個端點無需驗證即可使用,並描述 SCIM 伺服器的功能:
|
||||
|
||||
| 端點 | 說明 |
|
||||
|---|---|
|
||||
| `GET /api/v1/scim/v2/ServiceProviderConfig` | 伺服器功能與支援的特性 |
|
||||
| `GET /api/v1/scim/v2/Schemas` | User 與 Group 結構定義 |
|
||||
| `GET /api/v1/scim/v2/ResourceTypes` | 可用的資源類型(User、Group) |
|
||||
|
||||
`ServiceProviderConfig` 會宣告下列功能:
|
||||
|
||||
| 特性 | 是否支援 |
|
||||
|---|---|
|
||||
| Patch | 是 |
|
||||
| Bulk | 否 |
|
||||
| Filter | 是(最多 200 筆結果,僅 `eq` 運算子) |
|
||||
| Change password | 否 |
|
||||
| Sort | 否 |
|
||||
| ETag | 否 |
|
||||
|
||||
## 限制 {#limitations}
|
||||
|
||||
- **篩選**:僅支援 `eq` 運算子。複雜篩選、`and`/`or` 運算子、`co`(contains)與 `sw`(starts with)皆未實作。
|
||||
- **批次操作**:不支援。
|
||||
- **Sort 與 ETag**:不支援。
|
||||
- **角色**:SCIM 無法指派 SnapOtter 角色。所有佈建的使用者都會取得 `user` 角色。
|
||||
- **MAX_USERS**:SCIM 建立使用者時不會強制執行 `MAX_USERS` 環境變數限制。若您需要限制使用者數量,請在您的 IdP 中管理指派。
|
||||
- **單一權杖**:同一時間只能有一個 SCIM 權杖處於使用中狀態。若多個 IdP 需要 SCIM 存取權,它們必須共用該權杖。
|
||||
- **群組即團隊**:SCIM Group 對應到團隊,而非角色或權限群組。
|
||||
|
||||
## 疑難排解 {#troubleshooting}
|
||||
|
||||
### 403 "SCIM provisioning requires an enterprise license with the scim feature" {#_403-scim-provisioning-requires-an-enterprise-license-with-the-scim-feature}
|
||||
|
||||
您的授權未包含 `scim` 功能,或未設定任何授權。SCIM 需要企業版方案授權。請確認 `SNAPOTTER_LICENSE_KEY` 已設定,且該授權包含 `scim` 功能。
|
||||
|
||||
### 401 "Bearer token required" {#_401-bearer-token-required}
|
||||
|
||||
SCIM 請求未包含 `Authorization: Bearer <token>` 標頭。請檢查您 IdP 的佈建設定。
|
||||
|
||||
### 401 "Invalid token" {#_401-invalid-token}
|
||||
|
||||
權杖與已儲存的雜湊值不符。這會在權杖已被撤銷並重新產生時發生。請在您 IdP 的佈建設定中更新該權杖。
|
||||
|
||||
### 401 "SCIM not configured" {#_401-scim-not-configured}
|
||||
|
||||
尚未產生任何 SCIM 權杖。請使用 `POST /api/v1/enterprise/scim/token` 端點來建立一個。
|
||||
|
||||
### 409 "User already exists" / "userName already taken" {#_409-user-already-exists-username-already-taken}
|
||||
|
||||
已存在使用相同使用者名稱的使用者。這可能在 IdP 重試失敗的建立操作時發生。請在 SnapOtter 管理面板中檢查是否有重複的使用者名稱。
|
||||
|
||||
### 429 "SCIM rate limit exceeded" {#_429-scim-rate-limit-exceeded}
|
||||
|
||||
IdP 每分鐘傳送超過 1000 個請求。這通常在大型初始同步期間發生。大多數 IdP 會在速率限制視窗重設後自動重試。若問題持續存在,請檢查您 IdP 的佈建同步間隔。
|
||||
|
||||
### 使用者已解除佈建但未從 UI 移除 {#users-deprovisioned-but-not-removed-from-the-ui}
|
||||
|
||||
SCIM DELETE 是軟停用。已停用的使用者仍會以停用狀態顯示在管理員使用者清單中。這是刻意的設計,以便保留他們的資料。他們的角色會顯示為 `disabled:<original-role>`。
|
||||
@@ -0,0 +1,339 @@
|
||||
---
|
||||
description: "SnapOtter 的安全強化指南。容器安全、網路隔離、Docker secrets、Kubernetes 部署與合規產出物。"
|
||||
i18n_source_hash: 986f7658430c
|
||||
i18n_provenance: machine
|
||||
i18n_output_hash: c7ac676acc59
|
||||
---
|
||||
|
||||
# 安全與強化 {#security-hardening}
|
||||
|
||||
SnapOtter 完全在你的基礎架構上處理檔案。它預設會傳送匿名、不含內容的產品分析與當機報告,以協助改善這個專案。它絕不會傳送你的檔案、檔案名稱、檔案內容、OCR 輸出、影像中繼資料或文件文字。選用的意見回饋只在使用者提交後、且僅在分析啟用時才會傳送,聯絡欄位也只在明確同意聯絡時才會包含。管理員可在 Settings > System > Privacy 底下一鍵關閉分析與意見回饋擷取,無需重新建置。檔案處理始終留在你的容器內。
|
||||
|
||||
容器以專屬的非 root 使用者(`snapotter`)執行,並卸除除最低必需集之外的所有 Linux capabilities。完整的漏洞揭露政策與安全架構,請參閱 GitHub 上的 [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md)。
|
||||
|
||||
## 容器強化 {#container-hardening}
|
||||
|
||||
[預設的 docker-compose.yml](https://github.com/snapotter-hq/SnapOtter/blob/main/docker/docker-compose.yml) 包含正式環境安全強化。以下逐一說明每個選項及其重要性:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest
|
||||
ports:
|
||||
# Bind to localhost only for internet-facing deployments:
|
||||
- "127.0.0.1:1349:1349"
|
||||
volumes:
|
||||
- SnapOtter-data:/data
|
||||
- SnapOtter-workspace:/tmp/workspace
|
||||
environment:
|
||||
- AUTH_ENABLED=true
|
||||
- DEFAULT_PASSWORD=change-me-immediately
|
||||
- RATE_LIMIT_PER_MIN=1000
|
||||
- DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
# --- Resource limits ---
|
||||
mem_limit: 6g # Prevents runaway memory from crashing the host
|
||||
memswap_limit: 6g # No swap - fail fast instead of degrading the host
|
||||
cpus: 4 # Cap CPU usage to 4 cores
|
||||
pids_limit: 512 # Prevents fork bombs
|
||||
|
||||
# --- Capability restrictions ---
|
||||
cap_drop:
|
||||
- ALL # Drop ALL Linux capabilities first
|
||||
cap_add:
|
||||
- CHOWN # Needed for volume permission setup
|
||||
- SETUID # Needed for gosu privilege drop (root -> snapotter)
|
||||
- SETGID # Needed for gosu privilege drop
|
||||
- DAC_OVERRIDE # Needed for volume permission setup
|
||||
- FOWNER # Needed for volume permission setup
|
||||
|
||||
# --- Logging ---
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m" # Rotate logs at 50 MB
|
||||
max-file: "5" # Keep 5 rotated log files
|
||||
|
||||
# --- Health check ---
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "--max-time", "5", "http://localhost:1349/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 60s
|
||||
retries: 3
|
||||
|
||||
shm_size: "2gb" # Required for Python ML shared memory
|
||||
restart: unless-stopped
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_USER: snapotter
|
||||
POSTGRES_PASSWORD: snapotter
|
||||
POSTGRES_DB: snapotter
|
||||
volumes:
|
||||
- SnapOtter-pgdata:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U snapotter"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 15s
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- SnapOtter-redisdata:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
SnapOtter-data:
|
||||
SnapOtter-workspace:
|
||||
SnapOtter-pgdata:
|
||||
SnapOtter-redisdata:
|
||||
```
|
||||
|
||||
### 為何不設定 `no-new-privileges` {#why-no-new-privileges-is-not-set}
|
||||
|
||||
`security_opt: [no-new-privileges:true]` 是刻意省略的。進入點以 root 啟動以修正磁碟區擁有權,然後透過 [gosu](https://github.com/tianon/gosu) 降權至 `snapotter` 使用者,這需要 setuid。一旦降權完成,行程即以 `snapotter` 執行,並移除除上述五項之外的所有 capabilities。
|
||||
|
||||
如果你使用 Kubernetes 或 Docker 的 `--user` 旗標直接以非 root 執行(繞過 gosu),則 `no-new-privileges` 可安全啟用。
|
||||
|
||||
### 為何不設定 `read_only` {#why-read-only-is-not-set}
|
||||
|
||||
`read_only: true` 未設定,因為 PUID/PGID 重新對應會在啟動時寫入 `/etc/passwd` 和 `/etc/group`。如果你使用 Docker 的 `--user` 旗標或 Kubernetes `runAsUser` 而非 PUID/PGID,則可安全啟用唯讀根檔案系統。
|
||||
|
||||
## 網路隔離 {#network-isolation}
|
||||
|
||||
正常運作期間,容器不會建立**任何對外網路連線**。所有檔案處理都使用內建函式庫在本機完成。
|
||||
|
||||
```
|
||||
Browser --> Reverse Proxy (TLS) --> SnapOtter container --> (nothing)
|
||||
```
|
||||
|
||||
唯一的例外是 **AI 模型下載**:當使用者透過 UI 安裝 AI 功能套件組時,容器會從 Hugging Face 下載預先建置的套件組封存檔,外加一些來自 GitHub Releases、Google Storage 和 PyPI 的個別模型檔案。這些下載每個套件組只發生一次,並儲存在 `/data` 磁碟區中。
|
||||
|
||||
**防火牆建議:**
|
||||
|
||||
| 情境 | 對外規則 |
|
||||
|---|---|
|
||||
| 氣隙隔離(無 AI) | 封鎖容器的所有對外流量 |
|
||||
| 需要 AI 套件組 | 安裝期間允許對 `huggingface.co`、`*.xethub.hf.co`、`cdn-lfs.huggingface.co`、`github.com`、`objects.githubusercontent.com`、`storage.googleapis.com`、`pypi.org`、`files.pythonhosted.org` 的 HTTPS,之後封鎖 |
|
||||
| AI 安裝後 | 封鎖所有對外流量 — 模型已快取於本機 |
|
||||
|
||||
套件組封存檔由 Hugging Face 的 Xet 儲存提供,它透過 `*.xethub.hf.co` 端點平行傳輸,正是這讓數 GB 的套件組下載得以快速完成。如果你的防火牆允許 `huggingface.co` 但封鎖 `*.xethub.hf.co`,安裝仍會成功,但會回退至較慢的單串流下載,因此請將 Xet 主機加入允許清單以維持在快速路徑上。完全離線的安裝可略過這一切,改用 [離線套件組匯入](/zh-TW/guide/deployment)。
|
||||
|
||||
關於反向代理設定(Nginx、Traefik、Caddy、Cloudflare Tunnels),請參閱 [部署指南](/zh-TW/guide/deployment#reverse-proxy)。
|
||||
|
||||
## Docker Secrets {#docker-secrets}
|
||||
|
||||
就正式環境部署而言,請避免將 secrets 以純文字環境變數傳遞。進入點支援 Docker 的 `_FILE` 慣例:將 secret 掛載為檔案,並將對應的 `_FILE` 變數設為其路徑。
|
||||
|
||||
**支援的 secrets:**
|
||||
|
||||
| 變數 | `_FILE` 對應 |
|
||||
|---|---|
|
||||
| `DEFAULT_PASSWORD` | `DEFAULT_PASSWORD_FILE` |
|
||||
| `COOKIE_SECRET` | `COOKIE_SECRET_FILE` |
|
||||
| `OIDC_CLIENT_SECRET` | `OIDC_CLIENT_SECRET_FILE` |
|
||||
| `S3_ACCESS_KEY_ID` | `S3_ACCESS_KEY_ID_FILE` |
|
||||
| `S3_SECRET_ACCESS_KEY` | `S3_SECRET_ACCESS_KEY_FILE` |
|
||||
| `SNAPOTTER_LICENSE_KEY` | `SNAPOTTER_LICENSE_KEY_FILE` |
|
||||
|
||||
**使用 Docker Compose secrets 的範例:**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest
|
||||
environment:
|
||||
- AUTH_ENABLED=true
|
||||
- DEFAULT_USERNAME=admin
|
||||
- DEFAULT_PASSWORD_FILE=/run/secrets/snapotter_password
|
||||
- COOKIE_SECRET_FILE=/run/secrets/cookie_secret
|
||||
secrets:
|
||||
- snapotter_password
|
||||
- cookie_secret
|
||||
|
||||
secrets:
|
||||
snapotter_password:
|
||||
file: ./secrets/snapotter_password.txt
|
||||
cookie_secret:
|
||||
file: ./secrets/cookie_secret.txt
|
||||
```
|
||||
|
||||
::: tip
|
||||
Docker Compose secrets(不使用 Swarm)需要 Compose v2.23 或更新版本。
|
||||
:::
|
||||
|
||||
## Kubernetes 部署 {#kubernetes-deployment}
|
||||
|
||||
進入點會偵測容器是否已以非 root 執行(例如透過 Kubernetes `runAsUser`),並自動略過 gosu 降權。在此情況下它無法自行 chown 已掛載的磁碟區,因此它會驗證它們是否可寫入,若否則提早退出並提供可行動的指引,請參閱 [儲存權限](/zh-TW/guide/deployment#storage-permissions) 以了解 `fsGroup` 與外來 UID 設定(TrueNAS、OpenShift)。
|
||||
|
||||
**建議的 Pod SecurityContext:**
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: snapotter
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: snapotter
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: snapotter
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
fsGroup: 999
|
||||
containers:
|
||||
- name: snapotter
|
||||
image: snapotter/snapotter:latest
|
||||
ports:
|
||||
- containerPort: 1349
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
resources:
|
||||
requests:
|
||||
cpu: "1"
|
||||
memory: 2Gi
|
||||
limits:
|
||||
cpu: "4"
|
||||
memory: 6Gi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health
|
||||
port: 1349
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health
|
||||
port: 1349
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: workspace
|
||||
mountPath: /tmp/workspace
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: snapotter-data
|
||||
- name: workspace
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 2Gi
|
||||
```
|
||||
|
||||
由於 `runAsUser: 999` 是在 pod 層級設定的,進入點會完全略過 gosu。這讓 `allowPrivilegeEscalation: false` 和 `drop: [ALL]` capabilities 可以無衝突地使用。
|
||||
|
||||
關於資源規模,請參閱 [硬體需求](/zh-TW/guide/deployment#hardware-requirements)。
|
||||
|
||||
## 備份與復原 {#backup-and-recovery}
|
||||
|
||||
持久化狀態分散於兩個磁碟區:
|
||||
|
||||
| 磁碟區 | 內容 | 是否關鍵? |
|
||||
|---|---|---|
|
||||
| `SnapOtter-pgdata` | PostgreSQL 資料庫(使用者、設定、管線、作業、稽核記錄) | 是 |
|
||||
| `/data`(app 磁碟區) | 使用者上傳的檔案、AI 模型、Python venv | 部分(見下文) |
|
||||
|
||||
在 `/data` 磁碟區中:
|
||||
|
||||
| 路徑 | 內容 | 是否關鍵? |
|
||||
|---|---|---|
|
||||
| `/data/uploads/`、`/data/outputs/` | 使用者檔案與處理結果 | 是 |
|
||||
| `/data/ai/` | 已下載的 AI 模型檔案 | 否(可重新下載) |
|
||||
| `/data/venv/` | Python 虛擬環境 | 否(啟動時重建) |
|
||||
|
||||
### 資料庫備份 {#database-backup}
|
||||
|
||||
在堆疊執行期間,使用 `pg_dump` 備份資料庫:
|
||||
|
||||
```bash
|
||||
# Dump the database
|
||||
docker exec SnapOtter-postgres pg_dump -U snapotter snapotter > backup.sql
|
||||
|
||||
# Restore into a fresh database
|
||||
cat backup.sql | docker exec -i SnapOtter-postgres psql -U snapotter snapotter
|
||||
```
|
||||
|
||||
或者,停止堆疊並對 `SnapOtter-pgdata` 磁碟區建立快照:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker run --rm -v SnapOtter-pgdata:/data -v $(pwd)/backup:/backup \
|
||||
alpine tar czf /backup/snapotter-pgdata.tar.gz -C /data .
|
||||
```
|
||||
|
||||
### 使用者檔案備份 {#user-files-backup}
|
||||
|
||||
```bash
|
||||
# Snapshot the app data volume (excluding re-downloadable AI models)
|
||||
docker run --rm -v SnapOtter-data:/data -v $(pwd)/backup:/backup \
|
||||
alpine tar czf /backup/snapotter-files.tar.gz \
|
||||
--exclude='ai' --exclude='venv' -C /data .
|
||||
```
|
||||
|
||||
所有套件組的 AI 模型總計最多約 24 GB。由於它們可重新下載,請將 `/data/ai/` 和 `/data/venv/` 排除在備份之外以節省空間。只有資料庫與使用者檔案是關鍵的。
|
||||
|
||||
## 合規產出物 {#compliance-artifacts}
|
||||
|
||||
每次 SnapOtter 發行都包含下列安全產出物:
|
||||
|
||||
| 產出物 | 格式 | 取得位置 |
|
||||
|---|---|---|
|
||||
| SBOM(CycloneDX) | JSON | [GitHub Release](https://github.com/snapotter-hq/SnapOtter/releases) 資產:`snapotter-v{version}-sbom.cdx.json` |
|
||||
| SBOM(SPDX) | JSON | [GitHub Release](https://github.com/snapotter-hq/SnapOtter/releases) 資產:`snapotter-v{version}-sbom.spdx.json` |
|
||||
| 漏洞掃描 | Trivy JSON | [GitHub Release](https://github.com/snapotter-hq/SnapOtter/releases) 資產:`snapotter-v{version}-trivy.json` |
|
||||
| 漏洞掃描 | SARIF | [GitHub Security](https://github.com/snapotter-hq/SnapOtter/security) 分頁 |
|
||||
| 靜態分析 | CodeQL(JS/TS + Python) | [GitHub Security](https://github.com/snapotter-hq/SnapOtter/security) 分頁,每週 + 每次 PR 執行 |
|
||||
| 相依性審查 | GitHub 原生 | 每次 PR 檢查,在新增高嚴重性項目時失敗 |
|
||||
| Python 相依性稽核 | pip-audit | 每次推送的 CI 執行記錄 |
|
||||
| 安全政策 | Markdown | 儲存庫中的 [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md) |
|
||||
| 相依性更新 | Dependabot | 針對 npm、pip、Docker、Actions 的每週自動化 PR |
|
||||
|
||||
**執行你自己的掃描:**
|
||||
|
||||
從發行中下載 SBOM,並用你偏好的工具掃描它:
|
||||
|
||||
```bash
|
||||
# Scan with Grype using the CycloneDX SBOM
|
||||
grype sbom:snapotter-v1.17.2-sbom.cdx.json
|
||||
|
||||
# Scan with Trivy using the SPDX SBOM
|
||||
trivy sbom snapotter-v1.17.2-sbom.spdx.json
|
||||
|
||||
# Scan the Docker image directly
|
||||
trivy image snapotter/snapotter:1.17.2
|
||||
```
|
||||
|
||||
::: info
|
||||
SBOM 與漏洞掃描反映的是該發行所發布的確切映像檔。部署後安裝的 AI 模型套件組不包含在 SBOM 中,因為它們是在執行期間下載的。
|
||||
:::
|
||||
@@ -0,0 +1,239 @@
|
||||
---
|
||||
description: "涵蓋所有模態的支援檔案格式,55+ 種影像輸入格式,以及 video、audio、PDF 與檔案格式。"
|
||||
i18n_source_hash: e53ecf65be25
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: cd6ab9dec9f1
|
||||
---
|
||||
|
||||
# 支援的格式 {#supported-formats}
|
||||
|
||||
SnapOtter 可處理五種模態的檔案:影像、video、audio、PDF 與檔案。本頁列出所有支援的格式。
|
||||
|
||||
## 影像格式 {#image-formats}
|
||||
|
||||
SnapOtter 支援 55+ 種影像格式的輸入,以及 13 種格式的輸出。
|
||||
|
||||
## 輸入格式 {#input-formats}
|
||||
|
||||
### 網頁標準(9) {#web-standards-9}
|
||||
|
||||
| 格式 | 副檔名 | 解碼器 | 備註 |
|
||||
|--------|-----------|---------|-------|
|
||||
| JPEG | .jpg, .jpeg | Sharp(原生) | |
|
||||
| PNG | .png | Sharp(原生) | 擷取 APNG 第一幀 |
|
||||
| WebP | .webp | Sharp(原生) | |
|
||||
| GIF | .gif | Sharp(原生) | 支援動態 |
|
||||
| AVIF | .avif | Sharp(原生) | |
|
||||
| SVG | .svg | Sharp(librsvg) | 針對 XXE/SSRF 進行清理 |
|
||||
| SVGZ | .svgz | gunzip + Sharp | Gzip 炸彈防護 |
|
||||
| APNG | .apng | Sharp(原生) | 僅第一幀 |
|
||||
| JPEG XL | .jxl | djxl / ImageMagick | 雙層回退 |
|
||||
|
||||
### 專業(7) {#professional-7}
|
||||
|
||||
| 格式 | 副檔名 | 解碼器 | 備註 |
|
||||
|--------|-----------|---------|-------|
|
||||
| TIFF | .tiff, .tif | Sharp(原生) | 支援多頁 |
|
||||
| PSD | .psd | ImageMagick | 扁平化合成 |
|
||||
| EPS | .eps, .epsf | ImageMagick + Ghostscript | 300dpi 點陣化,安全強化 |
|
||||
| OpenEXR | .exr | ImageMagick | Linear-to-sRGB 轉換 |
|
||||
| Radiance HDR | .hdr | ImageMagick | Linear-to-sRGB 轉換 |
|
||||
| DPX | .dpx | ImageMagick | Log-to-sRGB 轉換 |
|
||||
| Cineon | .cin | ImageMagick | 電影/VFX 格式 |
|
||||
|
||||
### 相機 RAW(23) {#camera-raw-23}
|
||||
|
||||
| 格式 | 副檔名 | 相機品牌 | 解碼器 |
|
||||
|--------|-----------|-------------|---------|
|
||||
| DNG | .dng | Adobe(通用) | exiftool / ImageMagick + LibRaw |
|
||||
| CR2 | .cr2 | Canon(2018 年前) | exiftool / ImageMagick + LibRaw |
|
||||
| CR3 | .cr3 | Canon(2018 年起) | exiftool / ImageMagick + LibRaw |
|
||||
| NEF | .nef | Nikon | exiftool / ImageMagick + LibRaw |
|
||||
| NRW | .nrw | Nikon(Coolpix) | exiftool / ImageMagick + LibRaw |
|
||||
| ARW | .arw | Sony | exiftool / ImageMagick + LibRaw |
|
||||
| ORF | .orf | Olympus | exiftool / ImageMagick + LibRaw |
|
||||
| RW2 | .rw2 | Panasonic | exiftool / ImageMagick + LibRaw |
|
||||
| RAF | .raf | Fujifilm | exiftool / ImageMagick + LibRaw |
|
||||
| PEF | .pef | Pentax/Ricoh | exiftool / ImageMagick + LibRaw |
|
||||
| 3FR | .3fr | Hasselblad | exiftool / ImageMagick + LibRaw |
|
||||
| IIQ | .iiq | Phase One | exiftool / ImageMagick + LibRaw |
|
||||
| SRW | .srw | Samsung | exiftool / ImageMagick + LibRaw |
|
||||
| X3F | .x3f | Sigma | exiftool / ImageMagick + LibRaw |
|
||||
| RWL | .rwl | Leica | exiftool / ImageMagick + LibRaw |
|
||||
| GPR | .gpr | GoPro | exiftool / ImageMagick + LibRaw |
|
||||
| FFF | .fff | Hasselblad(舊版) | exiftool / ImageMagick + LibRaw |
|
||||
| MRW | .mrw | Minolta | exiftool / ImageMagick + LibRaw |
|
||||
| MEF | .mef | Mamiya | exiftool / ImageMagick + LibRaw |
|
||||
| KDC | .kdc | Kodak | exiftool / ImageMagick + LibRaw |
|
||||
| DCR | .dcr | Kodak | exiftool / ImageMagick + LibRaw |
|
||||
| ERF | .erf | Epson | exiftool / ImageMagick + LibRaw |
|
||||
| PTX | .ptx | Pentax(輕便型) | exiftool / ImageMagick + LibRaw |
|
||||
|
||||
### 現代格式(3) {#modern-formats-3}
|
||||
|
||||
| 格式 | 副檔名 | 解碼器 | 備註 |
|
||||
|--------|-----------|---------|-------|
|
||||
| JPEG 2000 | .jp2, .j2k, .j2c, .jpc, .jpf, .jpx | opj_decompress / ImageMagick | 數位電影、醫療影像 |
|
||||
| QOI | .qoi | 內嵌 TypeScript 編解碼器 | 遊戲開發、嵌入式系統 |
|
||||
| HEIC/HEIF | .heic, .heif | heif-convert / heif-dec | iPhone 照片 |
|
||||
|
||||
### 舊版/系統(4) {#legacy-system-4}
|
||||
|
||||
| 格式 | 副檔名 | 解碼器 | 備註 |
|
||||
|--------|-----------|---------|-------|
|
||||
| BMP | .bmp | ImageMagick | |
|
||||
| ICO | .ico | ImageMagick | 擷取最大圖層 |
|
||||
| CUR | .cur | ImageMagick | Windows 游標(ICO 變體) |
|
||||
| TGA | .tga | ImageMagick | 僅依副檔名偵測 |
|
||||
|
||||
### 科學與遊戲(2) {#scientific-and-gaming-2}
|
||||
|
||||
| 格式 | 副檔名 | 解碼器 | 備註 |
|
||||
|--------|-----------|---------|-------|
|
||||
| FITS | .fits, .fit, .fts | ImageMagick | 天文學(NASA 標準) |
|
||||
| DDS | .dds | ImageMagick | 遊戲紋理(DirectX) |
|
||||
|
||||
### 交換格式(6) {#interchange-6}
|
||||
|
||||
| 格式 | 副檔名 | 解碼器 | 備註 |
|
||||
|--------|-----------|---------|-------|
|
||||
| PPM | .ppm | Sharp(原生) | 彩色像素圖 |
|
||||
| PGM | .pgm | Sharp(原生) | 灰階 |
|
||||
| PBM | .pbm | Sharp(原生) | 1 位元點陣圖 |
|
||||
| PNM | .pnm | Sharp(原生) | 通用格式 |
|
||||
| PAM | .pam | Sharp(原生) | 任意映射 |
|
||||
| PFM | .pfm | Sharp(原生) | 浮點映射 |
|
||||
|
||||
## 輸出格式(13) {#output-formats-13}
|
||||
|
||||
| 格式 | 編碼器 | 品質控制 | 可用於 |
|
||||
|--------|---------|----------------|-------------|
|
||||
| JPEG | Sharp 原生 | 1-100 | 所有工具 |
|
||||
| PNG | Sharp 原生 | 壓縮 0-9 | 所有工具 |
|
||||
| WebP | Sharp 原生 | 1-100 | 所有工具 |
|
||||
| AVIF | Sharp 原生 | 1-100 | 所有工具 |
|
||||
| TIFF | Sharp 原生 | 1-100 | 完整轉換工具 |
|
||||
| GIF | Sharp 原生 | 1-100 | 完整轉換工具 |
|
||||
| JXL | Sharp 原生 | 1-100 | 所有工具 |
|
||||
| HEIC | heif-enc CLI | 1-100 | 完整轉換工具 |
|
||||
| HEIF | heif-enc CLI | 1-100 | 完整轉換工具 |
|
||||
| BMP | ImageMagick CLI | 無損 | Convert 工具 |
|
||||
| ICO | ImageMagick CLI | 無損 | Convert 工具 |
|
||||
| JP2 | opj_compress CLI | 壓縮比 | Convert 工具 |
|
||||
| QOI | 內嵌編解碼器 | 無損 | Convert 工具 |
|
||||
|
||||
## Video 格式 {#video-formats}
|
||||
|
||||
Video 的解碼與編碼由 FFmpeg(靜態建置)處理,因此輸入時支援每一種常見的容器與編解碼器。
|
||||
|
||||
### 輸入容器(15) {#input-containers-15}
|
||||
|
||||
| 格式 | 副檔名 | 典型編解碼器 | 備註 |
|
||||
|--------|-----------|----------------|-------|
|
||||
| MP4 | .mp4 | H.264, H.265, AV1 | 使用最廣泛的容器 |
|
||||
| QuickTime | .mov | H.264, ProRes | Apple 擷取/編輯 |
|
||||
| WebM | .webm | VP8, VP9, AV1 | 免權利金網頁格式 |
|
||||
| Matroska | .mkv | 任意 | 靈活的開放容器 |
|
||||
| AVI | .avi | 各種 | 舊版 Microsoft 容器 |
|
||||
| M4V | .m4v | H.264 | Apple MP4 變體 |
|
||||
| AVCHD | .mts | H.264 | 攝影機錄影 |
|
||||
| BDAV | .m2ts | H.264 | Blu-ray / AVCHD 傳輸串流 |
|
||||
| 3GP | .3gp | H.264, MPEG-4 | 行動裝置擷取 |
|
||||
| Flash Video | .flv | H.264, VP6 | 舊版串流 |
|
||||
| Windows Media | .wmv | VC-1, WMV | Windows Media |
|
||||
| MPEG | .mpg, .mpeg | MPEG-1, MPEG-2 | DVD 時代影片 |
|
||||
| MPEG-TS | .ts | MPEG-2, H.264 | 廣播傳輸串流 |
|
||||
| Ogg | .ogv | Theora | 開放的 Ogg 影片 |
|
||||
|
||||
### 輸出格式 {#output-formats}
|
||||
|
||||
| 格式 | 副檔名 | 影片編解碼器 | 由何者產生 |
|
||||
|--------|-----------|-------------|-------------|
|
||||
| MP4 | .mp4 | H.264 | Convert、compress 及大多數 video 工具 |
|
||||
| QuickTime | .mov | H.264 | Convert Video |
|
||||
| WebM | .webm | VP9 | Convert Video |
|
||||
| GIF | .gif | - | Video to GIF |
|
||||
| WebP | .webp | - | Video to WebP(動態) |
|
||||
|
||||
### 字幕 {#subtitles}
|
||||
|
||||
| 格式 | 副檔名 | 操作 |
|
||||
|--------|-----------|-----------|
|
||||
| SubRip | .srt | 內嵌、燒錄、擷取、自動產生 |
|
||||
| WebVTT | .vtt | 內嵌、燒錄、擷取、自動產生 |
|
||||
| ASS / SSA | .ass | 內嵌、燒錄(支援樣式) |
|
||||
|
||||
## Audio 格式 {#audio-formats}
|
||||
|
||||
Audio 同樣由 FFmpeg 處理。
|
||||
|
||||
### 輸入格式(11) {#input-formats-11}
|
||||
|
||||
| 格式 | 副檔名 | 壓縮 | 備註 |
|
||||
|--------|-----------|-------------|-------|
|
||||
| MP3 | .mp3 | 有損 | 通用相容性 |
|
||||
| WAV | .wav | 未壓縮(PCM) | 錄音室 / 編輯 |
|
||||
| FLAC | .flac | 無損 | 開放無損編解碼器 |
|
||||
| AAC | .aac | 有損 | 原始 AAC 串流 |
|
||||
| M4A | .m4a | 有損(AAC)/ 無損(ALAC) | MPEG-4 音訊 |
|
||||
| Ogg Vorbis | .ogg | 有損 | 開放格式 |
|
||||
| Opus | .opus | 有損 | 現代、低延遲 |
|
||||
| WMA | .wma | 有損 | Windows Media Audio |
|
||||
| AIFF | .aiff | 未壓縮(PCM) | Apple 未壓縮 |
|
||||
| AMR | .amr | 有損 | 語音 / 行動裝置 |
|
||||
| AC-3 | .ac3 | 有損 | Dolby Digital |
|
||||
|
||||
### 輸出格式 {#output-formats-1}
|
||||
|
||||
| 格式 | 副檔名 | 編解碼器 | 由何者產生 |
|
||||
|--------|-----------|-------|-------------|
|
||||
| MP3 | .mp3 | LAME | Convert Audio、Extract Audio |
|
||||
| WAV | .wav | PCM | Convert Audio、Extract Audio |
|
||||
| FLAC | .flac | FLAC(無損) | Convert Audio |
|
||||
| Ogg | .ogg | Vorbis | Convert Audio |
|
||||
| M4A | .m4a | AAC | Convert Audio、Extract Audio |
|
||||
|
||||
## 文件格式 {#document-formats}
|
||||
|
||||
文件處理使用 qpdf、LibreOffice、Ghostscript、Pandoc 與 WeasyPrint。
|
||||
|
||||
### 輸入格式(15) {#input-formats-15}
|
||||
|
||||
| 格式 | 副檔名 | 引擎 | 備註 |
|
||||
|--------|-----------|--------|-------|
|
||||
| PDF | .pdf | qpdf, Ghostscript, pdfcpu | 核心文件格式 |
|
||||
| Word | .docx, .doc | LibreOffice | Microsoft Word |
|
||||
| Excel | .xlsx, .xls | LibreOffice | Microsoft Excel |
|
||||
| PowerPoint | .pptx, .ppt | LibreOffice | Microsoft PowerPoint |
|
||||
| OpenDocument | .odt, .ods, .odp | LibreOffice | 文字、試算表、簡報 |
|
||||
| Rich Text | .rtf | LibreOffice | 跨應用程式 rich text |
|
||||
| Plain Text | .txt | LibreOffice, Pandoc | UTF-8 文字 |
|
||||
| Markdown | .md | Pandoc | CommonMark / GFM |
|
||||
| HTML | .html | WeasyPrint | 轉譯為 PDF |
|
||||
| EPUB | .epub | Pandoc, LibreOffice | 電子書格式 |
|
||||
|
||||
### 輸出格式 {#output-formats-2}
|
||||
|
||||
| 格式 | 副檔名 | 由何者產生 |
|
||||
|--------|-----------|-------------|
|
||||
| PDF | .pdf | Word/Excel/PowerPoint to PDF、Markdown to PDF、HTML to PDF |
|
||||
| PDF/A | .pdf | PDF/A Convert(封存) |
|
||||
| Word | .docx, .odt, .rtf, .txt | Convert Document、PDF to Word、Markdown to Word |
|
||||
| Presentation | .pptx, .odp | Convert Presentation |
|
||||
| Spreadsheet | .xlsx, .ods, .csv | Convert Spreadsheet |
|
||||
| HTML | .html | Markdown to HTML |
|
||||
| EPUB | .epub | Convert to EPUB |
|
||||
| Images | .png, .jpg | PDF to Image |
|
||||
|
||||
## 檔案格式 {#file-formats}
|
||||
|
||||
資料與封存工具可在結構化格式之間轉換,並打包檔案。
|
||||
|
||||
| 格式 | 副檔名 | 轉換 |
|
||||
|--------|-----------|-------------|
|
||||
| CSV | .csv | 與 JSON 及 Excel 互轉;分割與合併;從 XML 轉入 |
|
||||
| JSON | .json | 與 CSV、XML 及 YAML 互轉 |
|
||||
| XML | .xml | 與 JSON 互轉;轉為 CSV |
|
||||
| YAML | .yaml, .yml | 與 JSON 互轉 |
|
||||
| Excel | .xlsx | 與 CSV 互轉 |
|
||||
| ZIP | .zip | 建立封存、擷取內容 |
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
description: "SnapOtter 會收集哪些匿名使用資料、何時傳送,以及如何關閉整個執行個體的產品分析。"
|
||||
i18n_source_hash: 5d72dedaeb23
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 2f233be10af9
|
||||
---
|
||||
|
||||
# SnapOtter 收集的資料 {#what-snapotter-collects}
|
||||
|
||||
匿名產品分析預設為開啟,並由管理員針對整個執行個體設定。請在 Settings > System > Privacy 下將其關閉。
|
||||
|
||||
## 我們傳送的事件(啟用時) {#events-we-send-when-enabled}
|
||||
|
||||
- tool_used:工具 id、狀態、時長、類別、是否為 AI 工具,以及失敗時的錯誤代碼。
|
||||
- pipeline_executed:步驟數、工具 id、批次旗標、檔案數、時長、狀態。
|
||||
- ai_bundle_action:套件包 id、動作、時長。
|
||||
- 前端使用情形:開啟了哪些工具頁面、新增的檔案(僅計數)、已啟動的工具、下載、儲存、搜尋(僅結果數量)、已批次處理。
|
||||
- 當機回報:錯誤類型,以及僅含檔案基本名稱的來源堆疊。
|
||||
|
||||
## 我們絕不收集的資料 {#what-we-never-collect}
|
||||
|
||||
- 檔案名稱或路徑
|
||||
- 檔案內容
|
||||
- OCR 輸出文字
|
||||
- 影像中繼資料(EXIF)
|
||||
- 擷取的文件文字
|
||||
- 您的 IP 位址或帳號身分
|
||||
|
||||
## 關閉它 {#turning-it-off}
|
||||
|
||||
管理員:Settings > System > Privacy,將「Anonymous Product Analytics」關閉。它會立即停止,涵蓋整個執行個體。若要建置一個永遠無法發送的映像,請設定 `SNAPOTTER_ANALYTICS=off` build arg。
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
description: "SnapOtter 支援的 21 種語言,以及如何使用受 TypeScript 強制檢查的 i18n 系統來建立或改善翻譯。"
|
||||
i18n_source_hash: 55837d9fdaef
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: c06a7448962a
|
||||
---
|
||||
|
||||
# 翻譯指南 {#translation-guide}
|
||||
|
||||
SnapOtter 內建支援 21 種語言。其 i18n 系統採用輕量的自訂執行環境,具備受 TypeScript 強制檢查的語系完整性,以及動態程式碼分割。
|
||||
|
||||
## 支援的語言 {#supported-languages}
|
||||
|
||||
| Code | Language | Native Name | Direction |
|
||||
|------|----------|-------------|-----------|
|
||||
| `en` | English | English | LTR |
|
||||
| `zh-CN` | Chinese (Simplified) | 简体中文 | LTR |
|
||||
| `zh-TW` | Chinese (Traditional) | 繁體中文 | LTR |
|
||||
| `ja` | Japanese | 日本語 | LTR |
|
||||
| `ko` | Korean | 한국어 | LTR |
|
||||
| `es` | Spanish | Español | LTR |
|
||||
| `fr` | French | Français | LTR |
|
||||
| `it` | Italian | Italiano | LTR |
|
||||
| `pt-BR` | Portuguese (Brazil) | Português (Brasil) | LTR |
|
||||
| `de` | German | Deutsch | LTR |
|
||||
| `nl` | Dutch | Nederlands | LTR |
|
||||
| `sv` | Swedish | Svenska | LTR |
|
||||
| `ru` | Russian | Русский | LTR |
|
||||
| `pl` | Polish | Polski | LTR |
|
||||
| `uk` | Ukrainian | Українська | LTR |
|
||||
| `ar` | Arabic | العربية | RTL |
|
||||
| `tr` | Turkish | Türkçe | LTR |
|
||||
| `hi` | Hindi | हिन्दी | LTR |
|
||||
| `vi` | Vietnamese | Tiếng Việt | LTR |
|
||||
| `id` | Indonesian | Bahasa Indonesia | LTR |
|
||||
| `th` | Thai | ไทย | LTR |
|
||||
|
||||
## 語言偵測的運作方式 {#how-language-detection-works}
|
||||
|
||||
SnapOtter 採用三層解析順序:
|
||||
|
||||
1. **使用者偏好** - 儲存在 `localStorage("snapotter-locale")`,並在已驗證身分時同步至使用者設定
|
||||
2. **瀏覽器自動偵測** - 以 BCP 47 前綴比對走訪 `navigator.languages` 陣列
|
||||
3. **執行個體預設值** - 管理員的 `DEFAULT_LOCALE` 環境變數(從 `GET /api/v1/config/locale` 取得)
|
||||
4. **英文備援** - 永遠可用
|
||||
|
||||
使用者可從以下位置變更語言:
|
||||
- **頁尾的地球選擇器**(桌面版,永遠可見)
|
||||
- **登入頁面**的語言選擇器(登入前)
|
||||
- **設定 > 一般**區段(每位使用者的偏好)
|
||||
- **行動版側邊欄**的語言下拉選單
|
||||
- **設定 > 系統**區段可設定整個執行個體的預設值(僅限管理員)
|
||||
|
||||
## 翻譯的運作方式 {#how-translations-work}
|
||||
|
||||
所有 UI 字串都放在 `packages/shared/src/i18n/`。參考檔案是 `en.ts`,它匯出一個具型別的物件,包含應用程式使用的每個字串(約 1500 個鍵)。其他語言則是分開的檔案(例如 `de.ts`、`fr.ts`),匯出相同的結構。
|
||||
|
||||
`TranslationKeys` 型別使用 `DeepStringRecord` 接受任意字串值,同時強制檢查鍵結構。TypeScript 會在編譯時期抓出任何翻譯檔案中缺少的鍵。
|
||||
|
||||
執行階段只會透過動態 `import()` 載入目前使用中的語系,讓主要套件保持精簡。
|
||||
|
||||
## 在元件中使用翻譯 {#using-translations-in-components}
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { format, plural } from "@/lib/format";
|
||||
|
||||
function MyComponent() {
|
||||
const { t, locale, setLocale } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{t.common.settings}</h1>
|
||||
<p>{format(t.settings.people.deleteConfirm, { username: "admin" })}</p>
|
||||
<p>{plural(count, t.automate.fileCount, t.automate.fileCountPlural)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 貢獻翻譯 {#contributing-a-translation}
|
||||
|
||||
我們歡迎直接提交翻譯 PR。你可以改善既有的語系,或新增一個語系。
|
||||
|
||||
若要在不提交程式碼的情況下回報翻譯錯誤,請開一個 [GitHub Issue](https://github.com/snapotter-hq/SnapOtter/issues),並附上語言、錯誤的字串以及建議的修正。
|
||||
|
||||
::: tip
|
||||
翻譯 PR 不需要事先核准。Fork 儲存庫、進行你的變更,然後開一個 PR。完整的 PR 流程與 CLA 要求請參閱 [Contributing Guide](/zh-TW/guide/contributing)。
|
||||
:::
|
||||
|
||||
## 如何建立或更新翻譯 {#how-to-create-or-update-a-translation}
|
||||
|
||||
### 1. Fork 並複製 {#_1-fork-and-clone}
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<your-username>/snapotter.git
|
||||
cd snapotter
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 2. 複製參考檔案(僅限新語言){#_2-copy-the-reference-file-new-language-only}
|
||||
|
||||
若你是在改善既有的翻譯,請略過此步驟。
|
||||
|
||||
```bash
|
||||
cp packages/shared/src/i18n/en.ts packages/shared/src/i18n/XX.ts
|
||||
```
|
||||
|
||||
### 3. 翻譯字串 {#_3-translate-the-strings}
|
||||
|
||||
開啟你的新檔案並翻譯每個字串值。物件結構與鍵請保持完全相同。
|
||||
|
||||
```ts
|
||||
import type { TranslationKeys } from "./en.js";
|
||||
|
||||
export const xx: TranslationKeys = {
|
||||
common: {
|
||||
upload: "Your translation here",
|
||||
// ... translate all entries
|
||||
},
|
||||
// ... translate all sections
|
||||
} as const;
|
||||
```
|
||||
|
||||
規則:
|
||||
- 不要翻譯物件的鍵,只翻譯字串值
|
||||
- 在結尾保留 `as const`
|
||||
- 從 `./en.js` 匯入 `TranslationKeys` 並為你的匯出加上型別
|
||||
- 讓 `{variable}` 佔位符保持原樣
|
||||
- 陣列(`rotatingPhrases`、`progressMessages`)必須有相同數量的項目
|
||||
- 不要翻譯:SnapOtter、JPEG、PNG、WebP、EXIF、API 以及其他技術術語
|
||||
|
||||
### 4. 註冊語系(僅限新語言){#_4-register-the-locale-new-language-only}
|
||||
|
||||
在 `packages/shared/src/i18n/index.ts` 中將你的語系加入 `SUPPORTED_LOCALES`:
|
||||
|
||||
```ts
|
||||
{ code: "xx", name: "Language Name", nativeName: "Native Name", dir: "ltr" },
|
||||
```
|
||||
|
||||
### 5. 驗證 {#_5-verify}
|
||||
|
||||
```bash
|
||||
pnpm typecheck # catches missing or mistyped keys
|
||||
pnpm lint # formatting check
|
||||
pnpm dev # manually verify strings appear correctly
|
||||
```
|
||||
|
||||
### 6. 提交 {#_6-submit}
|
||||
|
||||
對 `main` 開一個 PR,標題類似 `feat(i18n): add Swedish translation` 或 `fix(i18n): correct German typos`。CLA 機器人會在你第一次貢獻時要求你簽署。
|
||||
|
||||
## 新增翻譯鍵 {#adding-new-translation-keys}
|
||||
|
||||
當你新增需要新 UI 字串的功能時:
|
||||
|
||||
1. 先將新鍵加入 `en.ts`(參考檔案)
|
||||
2. 執行 `pnpm typecheck` - 任何缺少新鍵的語系檔案都會失敗
|
||||
3. 將新鍵加入所有語系檔案(暫時以英文作為備援)
|
||||
|
||||
## 設定 {#configuration}
|
||||
|
||||
透過環境變數設定執行個體的預設語言:
|
||||
|
||||
```yaml
|
||||
DEFAULT_LOCALE: "de" # German as the default for all new users
|
||||
```
|
||||
|
||||
## 檔案參考 {#file-reference}
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `packages/shared/src/i18n/en.ts` | 英文字串(參考語系,約 1500 個鍵)|
|
||||
| `packages/shared/src/i18n/index.ts` | `SUPPORTED_LOCALES`、`loadTranslations()`、型別匯出 |
|
||||
| `packages/shared/src/i18n/<locale>.ts` | 各語言的翻譯檔案 |
|
||||
| `apps/web/src/contexts/i18n-context.tsx` | `I18nProvider`、`useTranslation()` hook |
|
||||
| `apps/web/src/lib/format.ts` | `format()`、`plural()`、`formatFileSize()` 輔助函式 |
|
||||
| `apps/api/src/routes/config.ts` | `GET /api/v1/config/locale` 公開端點 |
|
||||
|
||||
## 翻譯網站、文件與 API 參考 {#translating-the-web-surfaces}
|
||||
|
||||
上述的 21 種語言支援涵蓋的是**應用程式**。公開網站(snapotter.com)、這個文件網站以及 REST API 參考同樣會翻譯成全部 21 種語言,由另一條經過雜湊把關的流程處理,該流程重複使用 `packages/shared/src/i18n` 中相同的工具名稱與描述,因此各處的術語都保持一致。
|
||||
|
||||
### 預設為機器翻譯 {#machine-translated-by-default}
|
||||
|
||||
網站與文件上的每個非英文頁面在第一輪都是**機器翻譯**的(由 Claude Code 工作階段完成,而非第三方服務),並帶有一個小巧、可關閉的橫幅來說明這一點,還附上回到本頁的連結。這是刻意的:它能快速且誠實地推出全部 21 種語言,接著邀請社群去精修最重要的頁面。機器翻譯能傳達意思;人工審閱則讓文字讀起來自然。
|
||||
|
||||
### 流程如何決定要翻譯什麼 {#how-the-web-pipeline-decides}
|
||||
|
||||
每個可翻譯的英文原文單元都會被雜湊,且該雜湊值會與其翻譯一併儲存。每次執行時,流程會:
|
||||
|
||||
- 翻譯任何尚未有翻譯的單元,
|
||||
- 略過任何儲存的雜湊值仍與英文原文相符的單元,
|
||||
- 當**機器**單元的英文原文變更時,重新翻譯它,
|
||||
- 當經**人工**精修的單元的英文原文變更時,將其標記為 `stale`(需要審閱),而非覆蓋你的成果。
|
||||
|
||||
### 透過 PR 精修網頁翻譯 {#refining-a-web-translation-by-pr}
|
||||
|
||||
你精修網站、文件或 API 參考翻譯的方式,與改善應用程式語系相同:編輯產生的檔案並開一個 PR。
|
||||
|
||||
1. 找到你語言的產生翻譯:
|
||||
- 網站 UI 字串:`apps/landing/src/i18n/<locale>.json`
|
||||
- 某個文件頁面:`apps/docs/<locale>/**.md`
|
||||
- API 參考:`apps/api/src/openapi.<locale>.yaml`
|
||||
2. 編輯文字。程式碼、連結、`{placeholders}` 以及任何 `⸤I18N…⸥` 標記都請保持原樣;流程的驗證器會拒絕丟失或重新排序它們的翻譯。
|
||||
3. 開一個 PR。編輯一個單元會將其來源標記從 `machine` 翻轉為 `human`,因此流程在之後的執行中**絕不會覆蓋它**。若英文原文之後變更,你的單元會被標記為 `stale` 以供審閱,而不是被靜默取代。
|
||||
|
||||
若要在不提交程式碼的情況下回報翻譯錯誤,請開一個 [GitHub Issue](https://github.com/snapotter-hq/SnapOtter/issues),並附上頁面 URL、語言、錯誤的文字以及你建議的修正。
|
||||
|
||||
::: tip
|
||||
翻譯流程由維護者執行;你不需要 API 金鑰即可貢獻。只要編輯產生的檔案並開一個 PR。流程如何運作請參閱 [`scripts/i18n/README.md`](https://github.com/snapotter-hq/SnapOtter/blob/main/scripts/i18n/README.md)。
|
||||
:::
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
i18n_source_hash: 9a6abf3fc8ae
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 0b85ef707c86
|
||||
---
|
||||
# 從 1.x 升級到 2.0 {#upgrading-from-1-x-to-2-0}
|
||||
|
||||
SnapOtter 1.x 把所有東西都存放在單一 SQLite 檔案,並以單一容器執行。SnapOtter 2.0 改用 PostgreSQL 與 Redis。本指南會逐步說明如何把 1.x 安裝移到 2.0 而不遺失資料。
|
||||
|
||||
簡短版本:重複使用你現有的 `/data` 磁碟區,2.0 會在首次開機時自動匯入你的 1.x 資料庫。你的使用者、已儲存的檔案、設定、API 金鑰與管線都會一併帶過來。舊資料庫從不會被修改,所以你隨時都能回復。
|
||||
|
||||
::: tip 給我們 1.x 使用者的話
|
||||
你們許多人從第一天起就信任 SnapOtter,而你們的回饋形塑了這次的版本。2.0 在底層改動了很多,本指南的存在就是要讓這次搬遷不會讓你損失任何你在意的東西。你的帳號、檔案、設定、API 金鑰與管線都會延續下來,而你的舊資料庫永遠不會被動到。感謝你與我們一起升級。
|
||||
:::
|
||||
|
||||
## 開始前:備份整個 `/data` 磁碟區 {#before-you-start-back-up-the-whole-data-volume}
|
||||
|
||||
每次都先做這件事。備份**整個** `/data` 磁碟區,而不只是 `snapotter.db` 檔案。
|
||||
|
||||
原因如下。1.x 以 WAL 模式執行 SQLite,因此已停止的 1.x 容器常會把大部分已提交的資料留在 `snapotter.db-wal`,旁邊只有一個幾乎為空的 `snapotter.db`。只複製 `snapotter.db` 會抓到一個空的資料庫,並悄悄地遺失一切。磁碟區同時承載 `snapotter.db`、`snapotter.db-wal`、`snapotter.db-shm` 以及你的 `files/` 目錄,它們必須作為一整組一起搬移。
|
||||
|
||||
```bash
|
||||
# Adjust the volume name to match yours (see "Check your volume name" below).
|
||||
docker run --rm -v SnapOtter-data:/data -v "$PWD":/backup \
|
||||
alpine tar czf /backup/snapotter-1x-data.tgz -C /data .
|
||||
```
|
||||
|
||||
## 先升級到 1.17.2 {#upgrade-to-1-17-2-first}
|
||||
|
||||
在搬到 2.0 之前,先把你的 1.x 安裝升級到最新的 1.x 版本(1.17.2)。這能讓 1.x 執行它自己最後的結構描述遷移,如此 2.0 才會從一個已知且完整的結構描述進行匯入。從較舊的 1.x 直接升級到 2.0 並不受支援。
|
||||
|
||||
## 檢查你的磁碟區名稱 {#check-your-volume-name}
|
||||
|
||||
只有當 2.0 堆疊掛載的磁碟區與你 1.x 安裝所用的相同時,匯入程式才看得到你的資料。Docker 磁碟區名稱區分大小寫,較舊的 README 片段用的是小寫的 `snapotter-data`,而 Compose 檔案用的是 `SnapOtter-data`。請確認你用的是哪一個:
|
||||
|
||||
```bash
|
||||
docker volume ls | grep -i snapotter
|
||||
```
|
||||
|
||||
在你的 2.0 設定中使用那個確切的名稱。
|
||||
|
||||
## 路徑 A:單一容器(最快) {#path-a-single-container-quickest}
|
||||
|
||||
如果你以單一 `docker run` 執行 SnapOtter,就繼續那樣做。當你沒有設定 `DATABASE_URL` 或 `REDIS_URL` 時,2.0 會在容器內啟動一個內嵌的 PostgreSQL 與 Redis,並在首次開機時自動偵測並匯入 `/data/snapotter.db`。
|
||||
|
||||
```bash
|
||||
docker run -d --name snapotter -p 1349:1349 \
|
||||
-v SnapOtter-data:/data \
|
||||
snapotter/snapotter:latest
|
||||
```
|
||||
|
||||
留意日誌中類似這樣的一行:
|
||||
|
||||
```
|
||||
Imported 1.x SQLite database: {"tables":{"users":2,"teams":1,...},"blobs":{"present":1,"missing":0}}
|
||||
```
|
||||
|
||||
就這樣。用你既有的憑證登入。
|
||||
|
||||
## 路徑 B:Compose(正式環境建議) {#path-b-compose-recommended-for-production}
|
||||
|
||||
2.0 的 Compose 堆疊會執行三個服務(app、Postgres、Redis)。將你 1.x 的 `/data` 磁碟區重複用於 app 服務。app 會自動偵測 `/data/snapotter.db`,並在首次開機時把它匯入 Postgres。
|
||||
|
||||
```yaml
|
||||
services:
|
||||
SnapOtter:
|
||||
image: snapotter/snapotter:latest
|
||||
volumes:
|
||||
- SnapOtter-data:/data # your existing 1.x volume
|
||||
- SnapOtter-workspace:/tmp/workspace
|
||||
environment:
|
||||
- DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
|
||||
- REDIS_URL=redis://:snapotter@redis:6379
|
||||
# ...
|
||||
```
|
||||
|
||||
如果你寧可明確指向舊資料庫,請設定 `SQLITE_MIGRATE_PATH=/data/snapotter.db`。明確指定的路徑永遠優先於自動偵測。
|
||||
|
||||
## 先預覽匯入(選用) {#preview-the-import-first-optional}
|
||||
|
||||
若要在不寫入任何內容的情況下精確看到會匯入什麼,請對你的資料庫檔案執行一次 dry run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @snapotter/api migrate:sqlite -- /path/to/snapotter.db --dry-run
|
||||
```
|
||||
|
||||
它會印出每個資料表的列數、在磁碟上找到多少已儲存的資料庫檔案,以及任何它將正規化的作業狀態。它不需要執行中的 Postgres。
|
||||
|
||||
## 哪些會延續,哪些不會 {#what-carries-over-and-what-does-not}
|
||||
|
||||
會延續的:
|
||||
|
||||
- 使用者,以及登入的能力。密碼雜湊不變,因此相同的使用者名稱與密碼仍然有效。
|
||||
- 團隊、設定(包含你的執行個體識別)、角色、API 金鑰(它們會繼續運作),以及已儲存的管線。
|
||||
- 作業歷史記錄。
|
||||
- 你已儲存的檔案庫,包含記錄與實際檔案,因為 `/data/files` 在磁碟區上會被保留。
|
||||
|
||||
不會延續的:
|
||||
|
||||
- 登入工作階段。所有人都要在升級後重新登入一次。憑證不變,所以只是單次重新登入,僅此而已。
|
||||
- 舊處理作業的輸入與輸出檔案。那些位於暫時性的工作區,依設計已不存在。作業歷史記錄仍保留。
|
||||
- 來自 1.x 的每位使用者分析同意旗標,在 2.0 沒有對應項(2.0 的分析是執行個體層級的設定)。
|
||||
|
||||
## 關閉匯入 {#turning-the-import-off}
|
||||
|
||||
如果磁碟區上存在 `snapotter.db`,而你刻意想要一個全新的資料庫,請設定 `SQLITE_MIGRATE_PATH=off`。
|
||||
|
||||
## 如果你的 2.0 執行個體已經有資料 {#if-you-already-have-data-in-the-2-0-instance}
|
||||
|
||||
匯入程式只會在空資料庫上執行。如果你以全新方式啟動了 2.0(建立了資料),之後才掛載一個舊的 `snapotter.db`,2.0 會偵測到它但不會匯入,因為合併兩個資料集可能會在 ID 上發生衝突。你會在日誌中看到一則警告。要匯入 1.x 資料,你需要一個空的執行個體:
|
||||
|
||||
- 如果 2.0 執行個體只有預設管理員(你其實還沒真正用過它),請停止堆疊、移除 Postgres 磁碟區(`SnapOtter-pgdata`),然後在舊的 `/data` 存在的情況下重新開機。它會乾淨地匯入。這只會清除可拋棄的 Postgres 資料,不會動到你的 1.x 資料庫。
|
||||
- 如果 2.0 執行個體握有你想保留的真實資料,這兩個資料集無法自動合併。請匯出你需要的內容,並將 1.x 資料匯入到另一個全新的部署。
|
||||
|
||||
## 回復 {#rolling-back}
|
||||
|
||||
升級從不會修改或刪除你的 1.x `snapotter.db`。如果你需要退回 1.x,請對相同的磁碟區重新部署 1.x 映像。升級後你在 2.0 建立的任何內容都位於 Postgres,不會出現在 1.x 資料庫,所以若你打算回復,就要盡快進行。
|
||||
@@ -0,0 +1,264 @@
|
||||
---
|
||||
description: "在 SnapOtter 中管理使用者、內建與自訂角色、權限、API 金鑰、團隊、工作階段以及稽核日誌。"
|
||||
i18n_source_hash: 5e28af686c96
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 94865da24af7
|
||||
---
|
||||
|
||||
# 使用者、角色與權限 {#users-roles-permissions}
|
||||
|
||||
SnapOtter 內建三種角色、17 項細緻權限,並支援具備可選之每工具存取控制的自訂角色。本頁涵蓋完整的授權模型、API 金鑰範圍限制、團隊管理以及稽核日誌。
|
||||
|
||||
::: tip 相關頁面
|
||||
[OIDC / SSO](/zh-TW/guide/oidc) | [SAML SSO](/zh-TW/guide/saml) | [SCIM 佈建](/zh-TW/guide/scim) | [安全性與強化](/zh-TW/guide/security)
|
||||
:::
|
||||
|
||||
## 使用者 {#users}
|
||||
|
||||
### 建立使用者 {#creating-users}
|
||||
|
||||
管理員可透過管理面板或 `POST /api/auth/register` 端點建立使用者。每位使用者都有一個使用者名稱、角色、團隊指派,以及一個選用的電子郵件地址。
|
||||
|
||||
### 預設管理員 {#default-admin}
|
||||
|
||||
首次啟動時,SnapOtter 會建立一個預設管理員帳號。憑證來自環境變數:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `DEFAULT_USERNAME` | `admin` | 初始管理員帳號的使用者名稱 |
|
||||
| `DEFAULT_PASSWORD` | `admin` | 初始管理員帳號的密碼 |
|
||||
|
||||
預設管理員在首次登入時必須變更密碼。
|
||||
|
||||
### 驗證提供者 {#authentication-providers}
|
||||
|
||||
使用者可透過多種方式進行驗證:
|
||||
|
||||
- **本機** - 使用者名稱與密碼儲存在 SnapOtter 資料庫
|
||||
- **OIDC** - 任何 OpenID Connect 提供者(參閱 [OIDC / SSO](/zh-TW/guide/oidc))
|
||||
- **SAML** - SAML 2.0 身分提供者(參閱 [SAML SSO](/zh-TW/guide/saml))
|
||||
- **SCIM** - 從身分提供者自動佈建(參閱 [SCIM 佈建](/zh-TW/guide/scim))
|
||||
|
||||
### 停用驗證 {#disabling-authentication}
|
||||
|
||||
設定 `AUTH_ENABLED=false` 以完全停用驗證。在此模式下,所有請求都會使用一個具備 `admin` 角色的合成匿名使用者。不需要登入。
|
||||
|
||||
::: warning
|
||||
停用驗證會授予任何能連上該執行個體的人完整的管理員存取權。僅在受信任的環境中使用。
|
||||
:::
|
||||
|
||||
## 內建角色 {#built-in-roles}
|
||||
|
||||
SnapOtter 包含三種內建角色。它們無法被修改或刪除。
|
||||
|
||||
### Admin {#admin}
|
||||
|
||||
全部 17 項權限。對執行個體有完整控制權。
|
||||
|
||||
`tools:use` `files:own` `files:all` `apikeys:own` `apikeys:all` `pipelines:own` `pipelines:all` `settings:read` `settings:write` `users:manage` `teams:manage` `features:manage` `system:health` `audit:read` `compliance:manage` `webhooks:manage` `security:manage`
|
||||
|
||||
### Editor {#editor}
|
||||
|
||||
7 項權限。可使用所有工具並管理所有檔案與管線,但無法存取管理功能。
|
||||
|
||||
`tools:use` `files:own` `files:all` `apikeys:own` `pipelines:own` `pipelines:all` `settings:read`
|
||||
|
||||
### User {#user}
|
||||
|
||||
5 項權限。可使用工具並管理自己的資源。
|
||||
|
||||
`tools:use` `files:own` `apikeys:own` `pipelines:own` `settings:read`
|
||||
|
||||
## 權限參考 {#permissions-reference}
|
||||
|
||||
| Permission | Description |
|
||||
|---|---|
|
||||
| `tools:use` | 使用任何處理工具 |
|
||||
| `files:own` | 檢視並管理自己的檔案 |
|
||||
| `files:all` | 檢視並管理所有使用者的檔案 |
|
||||
| `apikeys:own` | 建立並管理自己的 API 金鑰 |
|
||||
| `apikeys:all` | 檢視所有使用者的 API 金鑰 |
|
||||
| `pipelines:own` | 建立並管理自己的管線 |
|
||||
| `pipelines:all` | 檢視並管理所有使用者的管線 |
|
||||
| `settings:read` | 檢視執行個體設定 |
|
||||
| `settings:write` | 修改執行個體設定 |
|
||||
| `users:manage` | 建立、更新並刪除使用者帳號 |
|
||||
| `teams:manage` | 建立、更新並刪除團隊 |
|
||||
| `features:manage` | 安裝並管理 AI 功能套組 |
|
||||
| `system:health` | 存取健康檢查與就緒狀態端點 |
|
||||
| `audit:read` | 檢視稽核日誌並列出角色 |
|
||||
| `compliance:manage` | 管理 GDPR 生命週期與合規功能 |
|
||||
| `webhooks:manage` | 設定對外 webhook |
|
||||
| `security:manage` | 管理安全性設定(IP 允許清單、SSO 強制執行) |
|
||||
|
||||
## 自訂角色 {#custom-roles}
|
||||
|
||||
具備 `security:manage` 權限的管理員可透過管理面板或角色 API 建立自訂角色。列出角色需要 `audit:read`。
|
||||
|
||||
### 建立自訂角色 {#creating-a-custom-role}
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:1349/api/v1/roles \
|
||||
-H "Authorization: Bearer si_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "reviewer",
|
||||
"description": "Can use tools and view all files",
|
||||
"permissions": ["tools:use", "files:own", "files:all", "settings:read"]
|
||||
}'
|
||||
```
|
||||
|
||||
角色名稱必須為 2 至 30 個字元,小寫英數字並可含連字號與底線。
|
||||
|
||||
### 保留給管理員的權限 {#admin-reserved-permissions}
|
||||
|
||||
有三項權限保留給內建角色,無法指派給自訂角色:
|
||||
|
||||
- `compliance:manage`
|
||||
- `webhooks:manage`
|
||||
- `security:manage`
|
||||
|
||||
角色 API 會拒絕任何包含這些權限的請求。只有內建的 `admin` 角色能存取它們。
|
||||
|
||||
### 工具層級權限 {#tool-level-permissions}
|
||||
|
||||
自訂角色可選擇性地限制使用者能存取哪些工具。有兩種模式可用:
|
||||
|
||||
| Mode | Behavior | License requirement |
|
||||
|---|---|---|
|
||||
| `category` | 依模態限制(image、video、audio、document、file) | 無(免費) |
|
||||
| `tool` | 依個別工具 ID 限制 | 需要 `per_tool_permissions` 企業版功能 |
|
||||
|
||||
當設定了 `tool` 模式但企業版功能不可用時,SnapOtter 會優雅降級並允許存取所有工具。
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "image-only",
|
||||
"permissions": ["tools:use", "files:own"],
|
||||
"toolPermissions": {
|
||||
"mode": "category",
|
||||
"allowed": ["image"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 刪除自訂角色 {#deleting-a-custom-role}
|
||||
|
||||
當自訂角色被刪除時,所有指派給它的使用者會自動重新指派到 `user` 角色。
|
||||
|
||||
## 團隊 {#teams}
|
||||
|
||||
團隊會將使用者分組,以進行儲存與保留管理。首次啟動時會建立一個 `Default` 團隊。
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | string | 唯一的團隊名稱(1 至 50 個字元) |
|
||||
| `storageQuota` | number | 每個團隊的儲存上限(位元組)(無需企業版也可運作) |
|
||||
| `retentionHours` | number | 在這麼多小時後自動刪除輸出(需要 `team_retention_overrides`,企業版) |
|
||||
| `legalHold` | boolean | 防止自動刪除團隊成員的檔案(需要 `legal_hold`,企業版) |
|
||||
|
||||
::: info
|
||||
`Default` 團隊無法刪除。仍有成員的團隊無法刪除。請先重新指派成員。
|
||||
:::
|
||||
|
||||
## API 金鑰 {#api-keys}
|
||||
|
||||
使用者可產生 API 金鑰以進行程式化存取。每把金鑰使用 `si_` 前綴,且只會在建立時顯示一次。
|
||||
|
||||
### 範圍限定的權限 {#scoped-permissions}
|
||||
|
||||
API 金鑰可選擇性地攜帶一個 `permissions` 陣列。設定後,某次請求的有效權限是使用者角色權限與金鑰範圍權限的**交集**。這表示 API 金鑰永遠無法提升到超過使用者自身的權限。
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:1349/api/v1/api-keys \
|
||||
-H "Authorization: Bearer si_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "CI pipeline key",
|
||||
"permissions": ["tools:use", "files:own"],
|
||||
"expiresAt": "2027-01-01T00:00:00Z"
|
||||
}'
|
||||
```
|
||||
|
||||
### 到期 {#expiration}
|
||||
|
||||
金鑰接受一個選用的 `expiresAt` 時間戳記。過期的金鑰會在驗證時被拒絕。
|
||||
|
||||
## 稽核日誌 {#audit-log}
|
||||
|
||||
SnapOtter 會在儲存於 `audit_log` 資料庫資料表的結構化稽核日誌中記錄與安全相關的事件。
|
||||
|
||||
### 檢視稽核日誌 {#viewing-the-audit-log}
|
||||
|
||||
```
|
||||
GET /api/v1/audit-log?page=1&limit=50&action=LOGIN_FAILED&from=2026-01-01T00:00:00Z&to=2026-12-31T23:59:59Z
|
||||
```
|
||||
|
||||
需要 `audit:read` 權限。支援分頁(`page`、`limit`)與篩選(`action`、`ip`、`from`、`to`)。
|
||||
|
||||
### 工具操作稽核 {#tool-operation-auditing}
|
||||
|
||||
::: warning
|
||||
預設**不會**記錄 `TOOL_EXECUTED` 事件。它們需透過下列兩種途徑之一選擇加入:
|
||||
|
||||
1. 將 `auditToolOperations` 管理設定設為 `true`。
|
||||
2. 持有具備 `audit_export` 功能的有效授權(team 與 enterprise 方案皆可使用)。
|
||||
|
||||
若不具備上述其一,個別的工具執行不會被記錄在稽核日誌中。
|
||||
:::
|
||||
|
||||
### 匯出 {#exporting}
|
||||
|
||||
```
|
||||
GET /api/v1/enterprise/audit/export?format=csv&from=2026-01-01T00:00:00Z
|
||||
```
|
||||
|
||||
需要 `audit:read` 權限與 `audit_export` 企業版功能(team 與 enterprise 方案皆可使用)。支援 CSV 與 JSON 格式,並可依 `action`、`actorId`、`targetType`、`targetId`、`from` 與 `to` 篩選。
|
||||
|
||||
### 防竄改簽章 {#tamper-resistant-signing}
|
||||
|
||||
啟用後,每筆稽核日誌項目都會以從 `DATA_ENCRYPTION_KEY` 衍生的 HMAC 簽章。這需要:
|
||||
|
||||
1. 在你的環境中設定 `DATA_ENCRYPTION_KEY`。
|
||||
2. 啟用 `tamperResistantAudit` 管理設定。
|
||||
3. 具備 `tamper_resistant_audit` 功能的企業版授權。
|
||||
|
||||
### 保留 {#retention}
|
||||
|
||||
設定 `AUDIT_RETENTION_DAYS` 以自動清除舊項目。預設值是 `0`,代表項目會無限期保留。
|
||||
|
||||
### 事件參考 {#event-reference}
|
||||
|
||||
| Event | Category |
|
||||
|---|---|
|
||||
| `LOGIN_SUCCESS`、`LOGIN_FAILED` | Authentication |
|
||||
| `OIDC_LOGIN_SUCCESS`、`OIDC_LOGIN_FAILED` | Authentication |
|
||||
| `SAML_LOGIN_SUCCESS`、`SAML_LOGIN_FAILED` | Authentication |
|
||||
| `LOGOUT` | Authentication |
|
||||
| `USER_CREATED`、`USER_UPDATED`、`USER_DELETED` | User management |
|
||||
| `PASSWORD_CHANGED`、`PASSWORD_RESET` | User management |
|
||||
| `MFA_ENROLLED`、`MFA_DISABLED`、`MFA_VERIFIED`、`MFA_VERIFY_FAILED` | MFA |
|
||||
| `MFA_CHALLENGE_ISSUED`、`MFA_RECOVERY_USED`、`MFA_RESET` | MFA |
|
||||
| `ROLE_CREATED`、`ROLE_UPDATED`、`ROLE_DELETED` | Roles |
|
||||
| `API_KEY_CREATED`、`API_KEY_DELETED` | API keys |
|
||||
| `SETTINGS_UPDATED`、`IP_ALLOWLIST_UPDATED` | Settings |
|
||||
| `FILE_UPLOADED`、`FILE_DELETED` | Files |
|
||||
| `TOOL_EXECUTED` | Tools (opt-in) |
|
||||
| `SCIM_USER_PROVISIONED`、`SCIM_USER_UPDATED`、`SCIM_USER_DEPROVISIONED` | SCIM |
|
||||
| `SCIM_GROUP_SYNCED` | SCIM |
|
||||
| `LEGAL_HOLD_APPLIED`、`LEGAL_HOLD_RELEASED` | Compliance |
|
||||
| `GDPR_EXPORT_INITIATED`、`GDPR_USER_PURGED`、`GDPR_TEAM_PURGED` | Compliance |
|
||||
| `CONFIG_EXPORTED`、`CONFIG_IMPORTED` | Configuration |
|
||||
|
||||
## 工作階段管理 {#session-management}
|
||||
|
||||
工作階段以 cookie 為基礎,由 `SESSION_DURATION_HOURS` 控制(預設:168 小時 / 7 天)。
|
||||
|
||||
### 角色變更會使工作階段失效 {#role-changes-invalidate-sessions}
|
||||
|
||||
當管理員變更某位使用者的角色時,該使用者所有作用中的工作階段都會被刪除。使用者必須重新登入才能取得新權限。
|
||||
|
||||
### 安全防護 {#safety-guards}
|
||||
|
||||
- **最後管理員保護**:最後剩下的一位管理員無法被降級為較低角色。若你嘗試這麼做,API 會回傳錯誤。
|
||||
- **防止自我刪除**:管理員無法透過 API 刪除自己的帳號。
|
||||
Reference in New Issue
Block a user