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:
SnapOtter
2026-07-11 13:52:47 +08:00
committed by GitHub
parent 00b651c9f8
commit 4963ab3bbd
3620 changed files with 306134 additions and 0 deletions
+123
View File
@@ -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)、OCRPaddleOCR/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 工作佇列(poolsimage、media、ai、docs、system)進行具並行控制的批次處理
- 使用者驗證、RBAC(admin/user 角色與完整權限集)、API 金鑰管理,以及速率限制
- 團隊管理 - 僅限 admin 的 CRUD;使用者透過其個人檔案上的 `team` 欄位指派至團隊
- 執行階段設定 - `settings` 資料表中的鍵值儲存,可控制 `disabledTools``enableExperimentalTools``loginAttemptLimit` 與其他運維旋鈕,無需重新部署
- 透過資料庫支援的設定進行自訂品牌與執行階段偏好設定
- 位於 `/api/docs` 的 Scalar/OpenAPI 文件
- 在正式環境中以 SPA 形式提供已建置的前端
主要相依套件:Fastify、Drizzle ORMpg-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 poolimage、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 一個 workerimage、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 請求都會直接使用它,並略過子程序產生的成本。
+164
View File
@@ -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 檔案。
+131
View File
@@ -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)
+185
View File
@@ -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-corenode-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` 磁碟區。
**選項 1pg_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.xSQLite)遷移 {#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`
+571
View File
@@ -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 以上 VRAM12 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 | NVIDIA8 GB 以上 VRAM(建議 12 GB |
| 磁碟 | 總計 ~35 GB |
NVIDIA GPUCUDA)能大幅加速吃重的 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 秒。HEICApple 的變體)快得多,僅需 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"
```
+234
View File
@@ -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 ORMpg-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 = 無限制) |
+158
View File
@@ -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 407012 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 |
| OCRPaddleOCR | 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 |
| OCRPaddleOCR | 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 支援,統一的映像。
你的資料與設定會保留在磁碟區中。
+176
View File
@@ -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` 用於單一使用者/自用而不需登入)。
+170
View File
@@ -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 ConnectOIDC)進行單一登入。使用者可以透過外部身分供應商(例如 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` | | 供應商的簽發者(issuerURL。必須支援 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 flowAuthorization 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)/範圍設定才能釋出宣告
+224
View File
@@ -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 ServiceACSURL。 |
| `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 要求回應與斷言兩者都必須簽章)
+298
View File
@@ -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.0System 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>`
+339
View File
@@ -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 發行都包含下列安全產出物:
| 產出物 | 格式 | 取得位置 |
|---|---|---|
| SBOMCycloneDX | JSON | [GitHub Release](https://github.com/snapotter-hq/SnapOtter/releases) 資產:`snapotter-v{version}-sbom.cdx.json` |
| SBOMSPDX | 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) 分頁 |
| 靜態分析 | CodeQLJS/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 中,因為它們是在執行期間下載的。
:::
+239
View File
@@ -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 | Sharplibrsvg | 針對 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 格式 |
### 相機 RAW23 {#camera-raw-23}
| 格式 | 副檔名 | 相機品牌 | 解碼器 |
|--------|-----------|-------------|---------|
| DNG | .dng | Adobe(通用) | exiftool / ImageMagick + LibRaw |
| CR2 | .cr2 | Canon2018 年前) | exiftool / ImageMagick + LibRaw |
| CR3 | .cr3 | Canon2018 年起) | exiftool / ImageMagick + LibRaw |
| NEF | .nef | Nikon | exiftool / ImageMagick + LibRaw |
| NRW | .nrw | NikonCoolpix | 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 | 建立封存、擷取內容 |
+31
View File
@@ -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。
+212
View File
@@ -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)。
:::
+117
View File
@@ -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 資料庫,所以若你打算回復,就要盡快進行。
+264
View File
@@ -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 刪除自己的帳號。