fix: release QA hardening across processing, media, security, and CI gates (#649)

A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
This commit is contained in:
SnapOtter
2026-07-27 15:37:30 +08:00
committed by GitHub
parent bc32f86a07
commit d10d0f544f
855 changed files with 54564 additions and 13092 deletions
+4 -3
View File
@@ -1,8 +1,9 @@
---
description: "SnapOtter 的 monorepo 結構、app 與套件架構、請求生命週期,以及資源占用。"
i18n_output_hash: 733f35af8cb1
i18n_source_hash: a53946e760b0
i18n_source_hash: 50e076925c4b
i18n_provenance: human
i18n_output_hash: 712a7e7cf078
i18n_hash_version: 2
---
# 架構 {#architecture}
@@ -52,7 +53,7 @@ Python 腳本位於 `packages/ai/python/` 中。大型可選模型包根據需
### API`apps/api` {#api-apps-api}
一個 Fastify v5 伺服器,公開橫跨五種模態(image、video、audio、PDF、file)的 241 個工具路由,負責處理:
一個 Fastify v5 伺服器,公開橫跨五種模態(image、video、audio、PDF、file)的 243 個工具路由,負責處理:
- 檔案上傳、暫存工作區管理,以及持久化檔案儲存
- 使用者檔案資料庫(`user_files` 資料表):預設情況下,已儲存的編輯會儲存為一個獨立的新檔案;而當你覆寫原始檔案時,則儲存為一個與父檔案連結的版本。它會記錄套用了哪些工具(`toolChain`),並為 Files 頁面自動產生縮圖
- 工具執行(將每個工具請求路由至影像引擎或 AI 橋接層)
+40 -17
View File
@@ -1,8 +1,9 @@
---
description: "所有 SnapOtter 環境變數及其預設值。設定驗證、儲存、AI 模型、分析等。"
i18n_source_hash: 8e9e9ca2840c
i18n_source_hash: 25970c776f7c
i18n_provenance: human
i18n_output_hash: e2ec72107c68
i18n_output_hash: 37bc153cf292
i18n_hash_version: 2
---
# 設定 {#configuration}
@@ -19,28 +20,51 @@ i18n_output_hash: e2ec72107c68
| `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`。 |
| `TRUST_PROXY` | `loopback,linklocal,uniquelocal` | 允許哪些對端透過 `X-Forwarded-For` 設定用戶端 IP。預設值只相信來自私有網路的對端,因此 Docker 網路或區域網路中的反向代理會被信任,而公開用戶端偽造的標頭則不會。只有當你自己掌控的代理以公開位址位於前方時,才設為 `true`。 |
### 驗證 {#authentication}
下方兩個布林值只接受 `true``false`。其他任何值,例如 `1``yes``on`,都會驗證失敗,伺服器會在開始監聽之前結束。
| 變數 | 預設值 | 說明 |
|---|---|---|
| `AUTH_ENABLED` | `false` | 設為 `true` 要求登入。Docker 映像檔預設為 `true`。 |
| `AUTH_ENABLED` | `true` | 要求登入。設為 `false` 可在完全沒有帳號的情況下執行,此時每個請求都具有 admin 權限,因此請僅限於可信任的網路。 |
| `DEFAULT_USERNAME` | `admin` | 初始 admin 帳號的使用者名稱。僅於首次執行時使用。 |
| `DEFAULT_PASSWORD` | `admin` | 初始 admin 帳號的密碼。請於首次登入後變更。 |
| `MAX_USERS` | `0`(無上限) | 註冊使用者帳號的最大數量。設為 0 為無上限。 |
| `SESSION_DURATION_HOURS` | `168` | 登入工作階段的存活時間(小時)(預設為 7 天)。 |
| `SKIP_MUST_CHANGE_PASSWORD` | - | 設為任何非空值即可略過首次登入時強制變更密碼的提示 |
| `SKIP_MUST_CHANGE_PASSWORD` | `false` | 設為 `true` 可略過首次登入時強制變更密碼的提示 |
### 儲存 {#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` | 持久化使用者檔案(已上傳影像、已儲存結果)的目錄。 |
| `STORAGE_MODE` | `local` | `local``s3`。S3MinIO 需要具備 s3_storage 功能的授權,以及下方的 `S3_*` 變數。 |
| `DATABASE_URL` | `postgres://snapotter:snapotter@localhost:5432/snapotter` | PostgreSQL 連線字串。Compose 堆疊會將它指向自己的 `postgres` 服務;把它(連同 `REDIS_URL`)留著不設定,即可進入內嵌模式。 |
| `REDIS_URL` | `redis://localhost:6379` | Redis 連線字串(用於 BullMQ 工作佇列)。Compose 會將它指向自己的 `redis` 服務。 |
| `WORKSPACE_PATH` | `./tmp/workspace` | 處理期間暫存檔案的目錄。會自動清理。映像檔會設為 `/tmp/workspace` |
| `FILES_STORAGE_PATH` | `./data/files` | 持久化使用者檔案(已上傳影像、已儲存結果)的目錄。映像檔會設為 `/data/files` |
### S3 物件儲存 {#s3-object-storage}
僅在 `STORAGE_MODE=s3` 時才會讀取。三個必填項只要漏掉任何一個,啟動就會失敗,並指出你漏掉的變數名稱。
| 變數 | 預設值 | 說明 |
|---|---|---|
| `S3_BUCKET` | (空) | 存放上傳檔案與輸出的儲存桶。必填。 |
| `S3_ACCESS_KEY_ID` | (空) | 存取金鑰。必填。在容器中也可以改用 `S3_ACCESS_KEY_ID_FILE` 掛載它。 |
| `S3_SECRET_ACCESS_KEY` | (空) | 祕密金鑰。必填。同樣的檔案慣例:`S3_SECRET_ACCESS_KEY_FILE`。 |
| `S3_REGION` | `us-east-1` | 儲存桶所在區域。 |
| `S3_ENDPOINT` | (空) | 供 MinIO、R2、Backblaze 及其他 S3 相容儲存使用的自訂端點。留空表示 AWS。 |
| `S3_FORCE_PATH_STYLE` | `false` | 對 MinIO 以及其他需要 `endpoint/bucket/key` 而非虛擬主機定址的儲存,請設為 `true`。 |
| `S3_PREFIX` | (空) | 金鑰前綴,可讓一個儲存桶容納多個執行個體。 |
### 靜態資料加密 {#encryption-at-rest}
| 變數 | 預設值 | 說明 |
|---|---|---|
| `DATA_ENCRYPTION_KEY` | (空) | 64 個十六進位字元(32 位元組)。用來加密資料庫中儲存的機敏設定。不是 64 個十六進位字元的值會在啟動時被拒絕。 |
| `DATA_ENCRYPTION_KEY_PREVIOUS` | (空) | 你正在輪替掉的舊金鑰,格式相同。輪替期間請同時設定兩者,讓既有資料列仍可解密,之後再移除這一個。 |
### 內嵌模式 {#embedded-mode}
@@ -59,16 +83,15 @@ i18n_output_hash: e2ec72107c68
| 變數 | 預設值 | 說明 |
|---|---|---|
| `MAX_UPLOAD_SIZE_MB` | `100` | 每次上傳的最大檔案大小(MB)。設為 0 為無上限。 |
| `MAX_BATCH_SIZE` | `100` | 單一批次請求中的最大檔案數。設為 0 為無上限。 |
| `MAX_UPLOAD_SIZE_MB` | `0`(無上限) | 每次上傳的最大檔案大小(MB)。設為 0 為無上限。已發布的映像檔出廠即為 `0`;從原始碼建置則是從 100 開始。 |
| `MAX_BATCH_SIZE` | `0`(無上限) | 單一批次請求中的最大檔案數。設為 0 為無上限。已發布的映像檔出廠即為 `0`;從原始碼建置則是從 100 開始。 |
| `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_SVG_SIZE_MB` | `50` | 淨化處理之前可接受的最大 SVG 大小(MB)。這裡的 `0` 與前後各列的含義不同:它不是把上限調高,而是完全移除解析前的大小限制,因此這一項請保持設定。 |
| `MAX_PDF_PAGES` | `0`(無上限) | PDF 轉影像時的最大 PDF 頁數。設為 0 為無上限。 |
### 清理 {#cleanup}
@@ -82,7 +105,7 @@ i18n_output_hash: e2ec72107c68
| 變數 | 預設值 | 說明 |
|---|---|---|
| `DEFAULT_THEME` | `light` | 新工作階段的預設主題。`light``dark`。 |
| `DEFAULT_THEME` | `light` | 新工作階段的預設主題。`light``dark``system`。 |
| `DEFAULT_LOCALE` | `en` | 預設介面語言。 |
| `DEFAULT_TOOL_VIEW` | `sidebar` | 預設工具版面。`sidebar``fullscreen`。 |
@@ -124,13 +147,13 @@ services:
image: postgres:17-alpine
environment:
POSTGRES_USER: snapotter
POSTGRES_PASSWORD: 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"]
test: ["CMD-SHELL", "pg_isready -U snapotter -d snapotter"]
interval: 10s
timeout: 5s
retries: 12
+5 -4
View File
@@ -1,8 +1,9 @@
---
description: "如何為 SnapOtter 做出貢獻。錯誤回報、功能請求、拉取請求以及 CLA 要求。"
i18n_source_hash: 528802503035
i18n_source_hash: 6c920a5f83e0
i18n_provenance: human
i18n_output_hash: 28b8b8d90d65
i18n_output_hash: 99d1ae43ef11
i18n_hash_version: 2
---
# 貢獻 {#contributing}
@@ -53,7 +54,7 @@ i18n_output_hash: 28b8b8d90d65
### 先決條件 {#prerequisites}
- Node.js 22+
- Node.js 22.22+
- pnpm 9+
- Python 3.11+(僅供 AI 工具使用)
- Docker(選用,供完整整合測試使用)
@@ -71,7 +72,7 @@ docker compose -f docker-compose.dev.yml up -d
# Install dependencies
pnpm install
# Start dev servers (web on :1349, API on :13490)
# Start dev servers (web on :1351, API on :13490)
pnpm dev
```
+35 -15
View File
@@ -1,8 +1,9 @@
---
description: "SnapOtter 的 PostgreSQL 資料庫結構、資料表、遷移,以及備份程序。"
i18n_source_hash: 50d5d4f220cf
i18n_provenance: human
i18n_output_hash: 34debcc24c5a
i18n_source_hash: a68264552836
i18n_provenance: machine
i18n_output_hash: 867212c1ca88
i18n_hash_version: 2
---
# 資料庫 {#database}
@@ -145,6 +146,17 @@ SnapOtter 使用 PostgreSQL 17 搭配 [Drizzle ORM](https://orm.drizzle.team/)
| `details` | jsonb | 動作特定的資料 |
| `createdAt` | timestamp | 動作時間 |
### user_preferences {#user-preferences}
依偏好設定名稱存放的個別使用者介面狀態。首頁的已釘選工具透過 `PUT /api/v1/preferences` 寫入此處。
| 欄位 | 型別 | 說明 |
|---|---|---|
| `userId` | text | 指向 users 的外鍵,連帶刪除。與 `key` 共同構成主鍵 |
| `key` | text | 偏好設定名稱。與 `userId` 共同構成主鍵 |
| `value` | jsonb | 偏好設定內容 |
| `updatedAt` | timestamp | 最後寫入時間 |
## 遷移 {#migrations}
Drizzle 處理結構遷移。遷移檔案位於 `apps/api/drizzle/`。在開發期間:
@@ -157,29 +169,37 @@ npx drizzle-kit migrate # apply pending migrations
在生產環境中,待處理的遷移會在啟動時自動套用。
## 備份與還原 {#backup-and-restore}
## 備份與還原{#backup-and-restore}
關聯式資料庫位於 Postgres 容器的 `SnapOtter-pgdata` 磁碟區,而非應用程式的 `/data` 磁碟區
關聯式資料庫位於 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
# Dump into PostgreSQL's portable custom archive format
docker exec SnapOtter-postgres \
pg_dump --format=custom --no-owner -U snapotter snapotter > snapotter.dump
test -s snapotter.dump
docker exec -i SnapOtter-postgres pg_restore --list < snapotter.dump >/dev/null
# Restore into a fresh database
cat backup.sql | docker exec -i SnapOtter-postgres psql -U snapotter snapotter
# Restore into a fresh/disposable target first and fail on the first SQL error
docker exec -i SnapOtter-postgres \
pg_restore --exit-on-error --clean --if-exists --no-owner \
-U snapotter -d snapotter < snapotter.dump
```
**選項 2:磁碟區快照**
此資料庫轉儲不包含以 `/data/files` 保存的庫物件或 Redis 中持久的 BullMQ 狀態。使用[安全性與強化](/zh-TW/guide/security#backup-and-recovery) 中的協調程序備份和還原這些內容。
**冷捲快照**
```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 .
# Stop every service first, then use your storage platform to snapshot the
# PostgreSQL, app-data, and Redis volumes as one crash-consistent set.
docker compose -f docker/docker-compose.yml stop
```
不要使用 `tar` 複製即時 PostgreSQL 資料目錄。按項目編寫磁碟區名稱前綴,因此從 `docker inspect` 或您的儲存平台解析已安裝的磁碟區 ID,而不是假設文字標籤 `SnapOtter-pgdata`
### 從 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`
+24 -13
View File
@@ -1,8 +1,9 @@
---
description: "使用 Docker 將 SnapOtter 部署到正式環境。硬體需求、GPU 設定,以及 Nginx、Traefik 和 Cloudflare 的反向代理設定。"
i18n_output_hash: d1dc1e293c3a
i18n_source_hash: 98172965118b
i18n_source_hash: 2a722f86da75
i18n_provenance: human
i18n_output_hash: 97aa47ef2602
i18n_hash_version: 2
---
# 部署 {#deployment}
@@ -47,7 +48,7 @@ services:
# - MAX_USERS=0 # Max user accounts
# --- Networking ---
# - TRUST_PROXY=true # Trust X-Forwarded-For headers (set false if not behind a proxy)
# - TRUST_PROXY=loopback,linklocal,uniquelocal # Which peers may set the client IP via X-Forwarded-For (default shown)
# --- Bind mount permissions ---
# - PUID=1000 # Match your host user's UID (run: id -u)
@@ -82,7 +83,7 @@ services:
- SnapOtter-pgdata:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U snapotter"]
test: ["CMD-SHELL", "pg_isready -U snapotter -d snapotter"]
interval: 10s
timeout: 5s
retries: 12
@@ -170,13 +171,13 @@ services:
container_name: SnapOtter-postgres
environment:
POSTGRES_USER: snapotter
POSTGRES_PASSWORD: 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"]
test: ["CMD-SHELL", "pg_isready -U snapotter -d snapotter"]
interval: 10s
timeout: 5s
retries: 12
@@ -207,13 +208,17 @@ volumes:
docker compose -f docker-compose-gpu.yml up -d
```
在記錄中確認 CUDA 是否偵測到:
### 驗證 GPU 加速 {#verify-gpu-acceleration}
檢查日誌中的 CUDA 檢測:
```bash
docker logs SnapOtter 2>&1 | head -20
# Look for: [gpu] CUDA available via torch
```
如果 AI 工具在 CPU 上運行,即使 `--gpus all` 和 NVIDIA Container Toolkit 設定正確,請從 **設定 → AI 功能** 重新安裝受影響的捆綁包(例如背景刪除)。安裝程式會恢復 ONNX 運行時的 GPU 版本,而由另一個捆綁包(例如轉錄)引入的僅 CPU 版本可能會在共享 AI 環境中隱藏。如果從 UI 重新安裝無法恢復舊映像上的 GPU,請參閱 [問題 #490](https://github.com/snapotter-hq/SnapOtter/issues/490) 中的手動修復。
## 硬體需求 {#hardware-requirements}
這些數字來自跨多種系統的效能測試,從搭載 NVIDIA RTX 4070 的現代 amd64 工作站,一直到 Raspberry Pi,在每台機器上執行整個工具目錄,並掃描 Docker 資源限制以找出實際的下限。
@@ -436,11 +441,11 @@ securityContext:
| `AUTH_ENABLED` | `true` | 啟用/停用登入需求 |
| `DEFAULT_USERNAME` | `admin` | 初始管理員使用者名稱 |
| `DEFAULT_PASSWORD` | `admin` | 初始管理員密碼(首次登入時強制變更) |
| `MAX_UPLOAD_SIZE_MB` | `100` | 每個檔案的上傳限制 |
| `MAX_BATCH_SIZE` | `100` | 每個批次請求的最大檔案數 |
| `MAX_UPLOAD_SIZE_MB` | `0`(無限制) | 每個檔案的上傳限制MB)。映像檔出廠即為 `0`;從原始碼建置則從 100 起 |
| `MAX_BATCH_SIZE` | `0`(無限制) | 每個批次請求的最大檔案數。映像檔出廠即為 `0`;從原始碼建置則從 100 起 |
| `RATE_LIMIT_PER_MIN` | `1000` | 每個 IP 每分鐘的 API 請求數(設為 0 以停用) |
| `MAX_USERS` | `0`(無限制) | 最大使用者帳號數 |
| `TRUST_PROXY` | `true` | 信任來自反向代理的 X-Forwarded-For 標頭 |
| `TRUST_PROXY` | `loopback,linklocal,uniquelocal` | 允許哪些對端透過 `X-Forwarded-For` 設定用戶端 IP。預設僅限私有網路 |
| `PUID` | `999` | 以此 UID 執行(用於繫結掛載權限) |
| `PGID` | `999` | 以此 GID 執行(用於繫結掛載權限) |
| `LOG_LEVEL` | `info` | 記錄詳細程度:fatal、error、warn、info、debug、trace |
@@ -483,7 +488,13 @@ curl http://localhost:1349/api/v1/health
## 反向代理 {#reverse-proxy}
SnapOtter 預設會設定 `TRUST_PROXY=true`,使速率限制與記錄使用來自 `X-Forwarded-For` 標頭的真實用戶端 IP。
`TRUST_PROXY` 預設為 `loopback,linklocal,uniquelocal`,因此 SnapOtter 只相信來自私有網路對端的 `X-Forwarded-For`。同一主機、Docker 網路或區域網路中的反向代理開箱即受信任,這表示速率限制、登入暴力破解限制、稽核記錄以及 enterprise 版的 IP 允許清單,都不必設定就能看到真實用戶端 IP。
只有當前置代理從**公開**位址連到 SnapOtter 時才設為 `TRUST_PROXY=true`,例如位於另一個網路的雲端負載平衡器。在直接曝露的執行個體上,這個值會讓 `request.ip` 落入攻擊者手中,因為不斷更換該標頭的呼叫端每次請求都能拿到全新的速率限制計數。
動手量測用戶端 IP 之前,有兩件事要知道。macOS 與 Windows 上的 Docker Desktop 透過使用者空間代理提供已發布的連接埠,會把所有來源位址改寫成虛擬機閘道 `192.168.65.1`,因此在那裡無論 `TRUST_PROXY` 設成什麼都拿不回真實用戶端;面向網際網路的部署請放在 Linux 上。而且在任何平台上,經由 `localhost` 連到已發布的連接埠都會被視為橋接閘道而非你的用戶端,所以 localhost 測試完全說明不了真實用戶端是如何歸屬的。`TRUST_PROXY` 各種取值的完整表格與 Docker Desktop 注意事項請見 [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md#client-ip-resolution-trust_proxy)。
對於下面的每個代理來說,有兩件事很重要:允許大型請求正文(上傳),並且不緩衝回應。響應緩衝代理會破壞 SSE 進度,更明顯的是,使大文件下載“開始但永遠不會完成”,因為代理在傳遞之前保存整個文件。 SnapOtter 在下載時發送 `X-Accel-Buffering: no`,因此即使在其他地方保留緩衝,nginx 也會對它們進行串流傳輸,但除 nginx 之外的代理需要明確停用回應緩衝(如下面的每個配置所示)。如果下載中途停止,首先要檢查前面的緩衝代理。
### Nginx {#nginx}
@@ -505,7 +516,7 @@ server {
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)
# 串流響應而不是緩衝:SSE 進度(批次、AI、功能安裝)和大檔案下載需要。
proxy_buffering off;
proxy_read_timeout 300s;
}
@@ -549,7 +560,7 @@ images.example.com {
}
```
`flush_interval -1` 停用回應緩衝,這是 SSE 進度事件(批次處理、AI 工具、功能安裝)所必需的。延長的逾時允許大檔案上傳 Caddy 提關閉連線之前完成
`flush_interval -1` 停用回應緩衝,這是 SSE 進度事件(批次、AI 工具、功能安裝)以及大檔案下載串流(而不是停滯)所必需的。延長的逾時允許完成大檔案上傳,而無需 Caddy 提關閉連線。
### Cloudflare Tunnels {#cloudflare-tunnels}
+19 -7
View File
@@ -1,8 +1,9 @@
---
description: "SnapOtter 的本機開發環境設定、指令、程式碼慣例,以及如何新增工具。"
i18n_source_hash: cb03724d2829
i18n_provenance: human
i18n_output_hash: 64a00be99b89
i18n_source_hash: 56acc1bf9a9b
i18n_provenance: machine
i18n_output_hash: d94d76633902
i18n_hash_version: 2
---
# 開發者指南 {#developer-guide}
@@ -11,12 +12,12 @@ i18n_output_hash: 64a00be99b89
## 先決條件 {#prerequisites}
- [Node.js](https://nodejs.org/) 22+
- [Node.js](https://nodejs.org/) 22.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+。
只有在你要處理 AI/ML 附屬程序(去背、放大、OCR)時,才需要 Python 3.11+。
## 設定 {#setup}
@@ -32,10 +33,10 @@ pnpm dev
| 服務 | URL | 說明 |
|----------|--------------------------|------------------------------------|
| 前端 | http://localhost:1349 | Vite 開發伺服器,代理 /api |
| 前端 | http://localhost:1351 | Vite 開發伺服器,代理 /api |
| 後端 | http://localhost:13490 | Fastify API(透過代理存取) |
在瀏覽器中開啟 http://localhost:1349。以 `admin` / `admin` 登入。你會在首次登入時被提示變更密碼。
在瀏覽器中開啟 http://localhost:1351。以 `admin` / `admin` 登入。你會在首次登入時被提示變更密碼。
## 專案結構 {#project-structure}
@@ -220,6 +221,17 @@ docker build -f docker/Dockerfile -t snapotter:latest .
DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile -t snapotter:latest .
```
## 發布版本域 {#release-version-domains}
SnapOtter 有意有三個版本域。發佈期間請勿將一個網域複製到另一個網域:
- 應用程式發布版本涵蓋根清單、所有私人工作區包和 `APP_VERSION`。 Semantic-release 提供此值,`pnpm version:sync <version>` 在應用程式發布之前更新每個工作區。
- OpenAPI `info.version` 是穩定的公開 API-主力合約。所有本地化規範都保留在 `<major>.0.0` 上以實現相容的應用程式版本,並且僅當 API 合約轉移到新的主要版本時才會更改。
- `docker/feature-manifest.json` 保留 `imageVersion: 2.0.0` 作為不可變的舊功能包儲存時代。這些 v2 存檔路徑不是應用程式套件版本。準確的 OCR 使用運行時格式 v3 並單獨記錄其應用程式發布來源。
`tests/unit/infra/release-version-policy.test.ts` 強制執行這些邊界。新版本的網域或遷移必須一起更新該合約和相關的工件遷移設計。
獨立的 API 和舊套件值位於 `config/release-version-policy.json` 中;應用程式版本同步絕對不能隱含重寫該原則檔。
## 環境變數 {#environment-variables}
完整清單請見[設定指南](/zh-TW/guide/configuration)。開發時的關鍵變數:
+8 -7
View File
@@ -1,8 +1,9 @@
---
description: "SnapOtter Docker 映像標籤、GPU 效能基準、版本鎖定,以及 AMD64 與 ARM64 的多平台支援。"
i18n_output_hash: 8b69b4ed0e1b
i18n_source_hash: fda322e78b4b
i18n_source_hash: 566e20ca07fc
i18n_provenance: human
i18n_output_hash: 1e0a57772b89
i18n_hash_version: 2
---
# Docker 映像 {#docker-image}
@@ -93,13 +94,13 @@ services:
image: postgres:17-alpine
environment:
POSTGRES_USER: snapotter
POSTGRES_PASSWORD: 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"]
test: ["CMD-SHELL", "pg_isready -U snapotter -d snapotter"]
interval: 10s
timeout: 5s
retries: 12
@@ -140,9 +141,9 @@ volumes:
| 標籤 | 說明 |
|-----|------------|
| `latest` | 最新版本 |
| `1.11.0` | 明確版本 |
| `1.11` | 1.11.x 中的最新修補版 |
| `1` | 1.x 中的最新次要版 |
| `2.1.0` | 明確版本 |
| `2.1` | 2.1.x 中的最新修補版 |
| `2` | 2.x 中的最新次要版 |
## 平台 {#platforms}
+28 -61
View File
@@ -1,8 +1,9 @@
---
description: "用一道 Docker 指令安裝 SnapOtter。包含 Docker Compose 設定、從原始碼建置,以及完整功能總覽。"
i18n_output_hash: 4e12779bd211
i18n_source_hash: 68bf7f60b68d
i18n_provenance: human
i18n_source_hash: 8040133a6982
i18n_provenance: machine
i18n_output_hash: c2b2ed21e05f
i18n_hash_version: 2
---
# 快速上手 {#getting-started}
@@ -17,7 +18,7 @@ i18n_provenance: human
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` 後自動關閉。
這個單一容器行它所需的一切:在沒有設定 `DATABASE_URL` 的情況下,它在環回介面(嵌入模式)上啟動自己的 PostgreSQL 和 Redis,並將所有資料保存在 `SnapOtter-data` 中。這是在家庭實驗室上嘗試 SnapOtter 或自架網站的最快方法。對於生產,請使用[規範的 Docker Compose 堆疊](#docker-compose),它將 PostgreSQL 和 Redis 保留在自的容器中。嵌入模式以 root 身份運行(預設),並在設定 `DATABASE_URL` 後自動關閉。
要安裝在 Raspberry Pi、舊筆電或小型 VPS 上?請參閱[低資源環境部署](/zh-TW/guide/low-resource),取得調校過的逐步教學,並了解受限硬體能有什麼表現。
@@ -40,7 +41,7 @@ SnapOtter 預設包含匿名產品分析。若要關閉它,請開啟 **Setting
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)。
需要 [NVIDIA 容器工具包](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)。當 CUDA 不可用時自動回退 CPU。目前AI 推理不支援透過 VA-API、Quick Sync 或 OpenCL 進行 Intel/AMD iGPU 加速請參閱 [Docker 標籤](/zh-TW/guide/docker-tags) 以了解基準。如果 AI 工具在 CPU 上運作(儘管 `--gpus all`),請參閱[驗證 GPU 加速](/zh-TW/guide/deployment#verify-gpu-acceleration)
:::
::: details 也在 GHCR 上
@@ -51,67 +52,33 @@ docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data ghcr.io/snap
兩個登錄檔在每次發行時都會發布相同的映像檔。
:::
## Docker Compose {#docker-compose}
## Docker 編寫 {#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
使用每個版本維護和測試的生產文件,而不是從此頁面複製縮寫的 Compose 範例:
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
```bash
install -d -m 700 snapotter && cd snapotter
curl --proto '=https' --tlsv1.2 -fsSLo docker-compose.yml \
https://raw.githubusercontent.com/snapotter-hq/SnapOtter/v2.1.0/docker/docker-compose.yml
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
# Keep generated service credentials out of shell history and world-readable files.
umask 077
POSTGRES_PASSWORD="$(openssl rand -hex 32)"
REDIS_PASSWORD="$(openssl rand -hex 32)"
printf 'POSTGRES_PASSWORD=%s\nREDIS_PASSWORD=%s\n' \
"$POSTGRES_PASSWORD" "$REDIS_PASSWORD" > .env
volumes:
SnapOtter-data:
SnapOtter-pgdata:
SnapOtter-redisdata:
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d --no-build
```
關於所有環境變數,請參閱 [Configuration](/zh-TW/guide/configuration)
規範的 [`docker/docker-compose.yml`](https://github.com/snapotter-hq/SnapOtter/blob/v2.1.0/docker/docker-compose.yml) 包括所有四個運行時卷、運行狀況檢查、資源限制、持久性 Redis 配置、固定資料庫/快取映像以及當前容器強化。首次登入後立即變更預設管理員密碼。對於可重現的部署,請將 SnapOtter 應用程式映像固定到您驗證的發布標籤或摘要,而不是遵循 `latest`
有關所有環境變量,請參閱[配置](/zh-TW/guide/configuration);有關機密、網路策略和備份指南,請參閱[安全性和強化](/zh-TW/guide/security)。
## 從原始碼建置 {#build-from-source}
**先決條件:** Node.js 22+、pnpm 9+、Docker(用於 Postgres + Redis)、Python 3.10+(用於 AI 功能)、Git。
**先決條件:** Node.js 22.22+、pnpm 9+、Docker(用於 Postgres + Redis)、Python 3.11+(用於 AI 功能)、Git。
```bash
git clone https://github.com/snapotter-hq/SnapOtter.git
@@ -121,7 +88,7 @@ pnpm install
pnpm dev
```
- 前端:[http://localhost:1349](http://localhost:1349)
- 前端:[http://localhost:1351](http://localhost:1351)
- 後端:[http://localhost:13490](http://localhost:13490)
## 你可以做什麼 {#what-you-can-do}
@@ -130,11 +97,11 @@ pnpm dev
| 模態 | 數量 | 範例工具 |
|----------|-------|---------------|
| **影像** | 105 | 調整大小、裁切、壓縮、轉換、去背、放大、OCR、浮水印、拼貼、上色、GIF 工具、格式預設 |
| **影像** | 107 | 調整大小、裁切、壓縮、轉換、去背、放大、OCR、浮水印、拼貼、上色、GIF 工具、格式預設 |
| **影片** | 57 | 修剪、裁切、壓縮、轉換、合併、擷取音訊、自動字幕、影片轉 GIF、調整大小、穩定化、格式預設 |
| **音訊** | 27 | 修剪、合併、轉換、正規化、雜訊抑制、轉錄、音高變換、淡入淡出、鈴聲製作、格式預設 |
| **PDF / 文件** | 42 | 合併、分割、壓縮、OCR、浮水印、遮蔽、Word 轉 PDF、Excel 轉 PDF、旋轉、保護、修復 |
| **檔案** | 10 | CSV 轉 JSON、JSON 轉 XML、合併 CSV、分割 CSV、建立 ZIP、解壓縮 ZIP、圖表製作、YAML/JSON |
| **PDF / 文件** | 29 | 合併、分割、壓縮、OCR、浮水印、遮蔽、Word 轉 PDF、Excel 轉 PDF、旋轉、保護、修復 |
| **檔案** | 23 | CSV 轉 JSON、JSON 轉 XML、合併 CSV、分割 CSV、建立 ZIP、解壓縮 ZIP、圖表製作、YAML/JSON |
### 管線 {#pipelines}
+4 -3
View File
@@ -1,7 +1,8 @@
---
i18n_source_hash: f5de74aee1b9
i18n_source_hash: 521c03a6416c
i18n_provenance: machine
i18n_output_hash: d23efa46bf73
i18n_output_hash: 26499f05dc2f
i18n_hash_version: 2
---
# 低資源環境部署 {#low-resource-setups}
@@ -59,7 +60,7 @@ services:
image: postgres:17-alpine
environment:
- POSTGRES_USER=snapotter
- POSTGRES_PASSWORD=snapotter
- POSTGRES_PASSWORD=snapotter # 針對非本地部署更改此設置
- POSTGRES_DB=snapotter
volumes:
- ./postgres-data:/var/lib/postgresql/data
+12 -7
View File
@@ -1,8 +1,9 @@
---
description: "設定 SCIM 2.0 佈建,將使用者與群組從您的身分提供者同步至 SnapOtter。涵蓋 Okta、Azure AD / Entra ID 以及自訂整合。"
i18n_source_hash: bbd50119ec12
i18n_source_hash: 06ee702b386e
i18n_provenance: human
i18n_output_hash: 674e1fe7bfc1
i18n_output_hash: da12b859e973
i18n_hash_version: 2
---
# SCIM 佈建 {#scim-provisioning}
@@ -17,7 +18,7 @@ SCIM 佈建需要具備 `scim` 功能的 **enterprise** 授權。team 方案無
- 一個可透過公開網址存取的執行中 SnapOtter 執行個體
- 具備 `scim` 功能的企業版授權金鑰
- SnapOtter 的管理員存取權(產生或撤銷 SCIM 權杖需要 `users:manage` 權限)
- 內建 SnapOtter `admin` 帳戶及其完整的有效權限集。委託的自訂角色或缺少任何管理權限的管理 API 金鑰無法產生或撤銷全域 SCIM 令牌。
- 您身分提供者佈建設定的管理員存取權
## 快速開始 {#quick-start}
@@ -34,7 +35,7 @@ curl -X POST https://photos.example.com/api/v1/enterprise/scim/token \
```json
{
"token": "a1b2c3d4e5f6...",
"token": "so_scim_v2_a1b2c3d4e5f6...",
"message": "Save this token - it cannot be retrieved again"
}
```
@@ -49,15 +50,19 @@ SCIM 端點使用專屬的 Bearer 權杖,與使用者工作階段及 API 金
### 產生權杖 {#generating-a-token}
`POST /api/v1/enterprise/scim/token` 產生一個新的 SCIM 權杖。此端點需要具備 `users:manage` 權限的有效工作階段
`POST /api/v1/enterprise/scim/token` 產生新的 SCIM 代幣。由於令牌可以跨實例配置和變更用戶,因此此端點需要具有完整有效管理權限集的內建 `admin` 角色。將 `users:manage` 保留在自訂角色中是不夠的
該權杖以明文回傳,且僅回傳一次。SnapOtter 只儲存 scrypt 雜湊值。若您遺失權杖,請撤銷它並產生新的權杖。
同一時間只有一個 SCIM 權杖處於使用中狀態。產生新權杖會取代先前的權杖。
::: warning 升級後重新發行令牌
舊版未版本控制的 SCIM 令牌將被拒絕。升級到頒發 `so_scim_v2_...` 令牌的版本後,請產生新令牌並更新您的身分提供者,然後再恢復配置。
:::
### 撤銷權杖 {#revoking-a-token}
`DELETE /api/v1/enterprise/scim/token` 撤銷目前的 SCIM 權杖。此端點同樣需要 `users:manage`
`DELETE /api/v1/enterprise/scim/token` 撤銷目前的 SCIM 令牌。它具有與令牌生成相同的完整內建管理要求
### 速率限制 {#rate-limiting}
@@ -279,7 +284,7 @@ SCIM 請求未包含 `Authorization: Bearer <token>` 標頭。請檢查您 IdP
### 401 "Invalid token" {#_401-invalid-token}
權杖與已儲存的雜湊不符。這會在權杖已被撤銷並重新產生時發生。請在您 IdP 的佈建設定中更新該權杖
令牌格式錯誤、使用已停用的未版本化格式或與儲存的雜湊不符。產生目前的 `so_scim_v2_...` 令牌並在 IdP 的組態設定中更新該令牌
### 401 "SCIM not configured" {#_401-scim-not-configured}
+93 -165
View File
@@ -1,8 +1,9 @@
---
description: "SnapOtter 的安全強化指南。容器安全、網路隔離、Docker secrets、Kubernetes 部署與合規產出物。"
i18n_source_hash: 986f7658430c
i18n_provenance: human
i18n_output_hash: c7ac676acc59
i18n_source_hash: 9ff337fa0417
i18n_provenance: machine
i18n_output_hash: 0ec192d6b1ef
i18n_hash_version: 2
---
# 安全與強化 {#security-hardening}
@@ -11,133 +12,42 @@ SnapOtter 完全在你的基礎架構上處理檔案。它預設會傳送匿名
容器以專屬的非 root 使用者(`snapotter`)執行,並卸除除最低必需集之外的所有 Linux capabilities。完整的漏洞揭露政策與安全架構,請參閱 GitHub 上的 [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md)。
## 容器化 {#container-hardening}
## 容器化 {#container-hardening}
[預設的 docker-compose.yml](https://github.com/snapotter-hq/SnapOtter/blob/main/docker/docker-compose.yml) 包含正式環境安全強化。以下逐一說明每個選項及其重要性:
規範的 [CPU](https://github.com/snapotter-hq/SnapOtter/blob/main/docker/docker-compose.yml) 和 [GPU](https://github.com/snapotter-hq/SnapOtter/blob/main/docker/docker-compose-gpu.yml) Compose 檔案是事實來源。不要將縮寫範例複製到生產中;從您驗證的發布標籤部署檔案。
```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
- 記憶體、交換、CPU 和 PID 限制包含失控的本機處理。
- 每個服務都會放棄所有 Linux 功能。該應用程式僅添加回 `CHOWN, SETUID, SETGID, DAC_OVERRIDE, FOWNER, KILL` 來實現卷所有權、單向 `gosu` 身份刪除以及優雅的信號轉發。 PostgreSQL 和 Redis 僅接收其官方入口點所需的子集。
- `security_opt: [no-new-privileges:true]` 防止應用程式、PostgreSQL 和 Redis 容器中的進程獲得額外權限。這仍然與 `gosu` 相容:入口點以 root 身份開始,準備卷,並且僅下降到專用的 `snapotter` 用戶。
- PostgreSQL 和 Redis 影像輸入由摘要固定。應用程式同樣應該固定到經過驗證的發布標籤或摘要,而不是 `latest`
- 健康檢查、有界 JSON 日誌輪替、持久的 Redis AOF 和重啟策略在規範文件中集中定義。
# --- 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
對於面向網際網路的部署,將連接埠 1349 綁定到環回並在維護的反向代理處終止 TLS。產生唯一的 PostgreSQL 和 Redis 憑證,將機密儲存在受保護的檔案或機密管理器中,並立即變更初始管理員密碼。
# --- Logging ---
logging:
driver: json-file
options:
max-size: "50m" # Rotate logs at 50 MB
max-file: "5" # Keep 5 rotated log files
### 為什麼 `read_only` 沒有設定 {#why-read-only-is-not-set}
# --- 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
未設定 `read_only: true`,因為 PUID/PGID 重新映射在啟動時寫入 `/etc/passwd``/etc/group`。如果您使用 Docker 的 `--user` 標誌或 Kubernetes `runAsUser` 而不是 PUID/PGID,則可以安全地啟用只讀根檔案系統。
shm_size: "2gb" # Required for Python ML shared memory
restart: unless-stopped
## 網路隔離{#network-isolation}
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
文件處理是本地的,但預設安裝**不是無出口系統**。啟用遙測功能時,匿名產品分析使用 PostHog,崩潰報告使用 Sentry。設定 `SNAPOTTER_TELEMETRY=0`(或在「設定」>「系統」>「隱私權」下停用分析)以關閉兩者。 SnapOtter 絕不會在這些事件中包含上傳的檔案、檔案名稱、OCR 輸出、文件文字或其他文件內容。
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 捆綁包/模型安裝下載簽名發布輸入; URL導入獲取用戶請求的公共URL;並明確配置的 OIDC、SAML、OpenTelemetry、webhooks、S3 兼容存儲或類似集成會聯繫管理員選擇的目標。執行階段模型下載預設為停用。只有在明確選擇啟用自動備援下載時,才設定 `SNAPOTTER_ALLOW_MODEL_DOWNLOAD=1`。[離線捆綁導入](/zh-TW/guide/deployment) 可以在沒有運行時模型出口的情況下提供 AI 功能。
**防火牆建議:**
| 情境 | 對外規則 |
|設想|出站規則|
|---|---|
| 氣隙隔離(無 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 安裝後 | 封鎖所有對外流量 — 模型已快取於本機 |
|氣隙|設定`SNAPOTTER_TELEMETRY=0``SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0`,使用離線AI捆綁導入,停用URL導入和外部集成,然後阻止出口|
|預設遙測|允許瀏覽器/網頁日誌列出的 PostHog 和 Sentry 端點;如果策略不允許,則停用遙測|
|需要 AI 捆綁包|安裝過程中,允許HTTPS到`huggingface.co, *.xethub.hf.co, cdn-lfs.huggingface.co, github.com, objects.githubusercontent.com, storage.googleapis.com, pypi.org, files.pythonhosted.org`;然後阻止這些主機|
|外部集成|僅允許管理員準確配置的 OIDC/SAML/OTLP/webhook/物件儲存目標|
套件組封存檔由 Hugging Face 的 Xet 儲存提供,透過 `*.xethub.hf.co` 端點行傳輸,正是這讓數 GB 的套件組下載得以快速完成。如果的防火牆允許 `huggingface.co`封鎖 `*.xethub.hf.co`,安裝仍會成功,但會回退較慢的單流下載,因此將 Xet 主機加入允許清單以維持在快速路徑。完全離線安裝可略過這一切,改用 [離線套件組匯](/zh-TW/guide/deployment)。
捆綁包檔案由 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)。
關反向代理配置Nginx、Traefik、Caddy、Cloudflare Tunnels),請參閱[部署指南](/zh-TW/guide/deployment#reverse-proxy)。
## Docker Secrets {#docker-secrets}
@@ -255,85 +165,103 @@ spec:
關於資源規模,請參閱 [硬體需求](/zh-TW/guide/deployment#hardware-requirements)。
## 備份與原 {#backup-and-recovery}
## 備份與原 {#backup-and-recovery}
持久化狀態分散於兩個磁碟區:
生產 Compose 堆疊定義了四個磁碟區。在進行協調備份之前停止入口並讓活動作業完成,以便 PostgreSQL、Redis 和檔案狀態描述相同的時間點。
| 磁碟區 | 內容 | 是否關鍵? |
|體積|內容|復健治療|
|---|---|---|
| `SnapOtter-pgdata` | PostgreSQL 資料庫(使用者、設定、管、作業、稽核記錄) | 是 |
| `/data`(app 磁碟區) | 使用者上傳的檔案、AI 模型、Python venv | 部分(見下文) |
|`SnapOtter-pgdata`|PostgreSQL 使用者、設定、管、作業、文件元資料和審核日誌|批判的;使用快速故障邏輯轉儲進行可移植恢復|
|`SnapOtter-data`|保存的庫物件、日誌和 AI 狀態 (`/data/files, /data/logs, /data/ai, /data/ai/venv`)|備份整個磁碟區;為了節省空間,故意省略所有 AI 狀態並重新安裝其捆綁包|
|`SnapOtter-redisdata`|Redis AOF 用於持久的 BullMQ 隊列狀態|暫停應用程式並強制`SAVE`後備份;需要準確地恢復排隊的工作|
|`SnapOtter-workspace`|暫存物件儲存鍵 (`/tmp/workspace/uploads, /tmp/workspace/outputs`)|所有作業清空或取消後不要進行備份;當工作處於活動狀態時切勿丟棄它|
`/data` 磁碟區中:
Compose 通常在磁碟區名稱前加上項目名稱作為前綴。從已安裝的容器中解析真實的來源卷,而不是假設顯示名稱(例如 `SnapOtter-data`)是 Docker 磁碟區名稱。
| 路徑 | 內容 | 是否關鍵? |
|---|---|---|
| `/data/uploads/``/data/outputs/` | 使用者檔案與處理結果 | 是 |
| `/data/ai/` | 已下載的 AI 模型檔案 | 否(可重新下載) |
| `/data/venv/` | Python 虛擬環境 | 否(啟動時重建) |
### 資料庫備份{#database-backup}
### 資料庫備份 {#database-backup}
在堆疊執行期間,使用 `pg_dump` 備份資料庫:
使用 PostgreSQL 的自訂存檔格式並在將備份視為完整之前驗證存檔:
```bash
# Dump the database
docker exec SnapOtter-postgres pg_dump -U snapotter snapotter > backup.sql
docker exec SnapOtter-postgres \
pg_dump --format=custom --no-owner -U snapotter snapotter > snapotter.dump
test -s snapotter.dump
docker exec -i SnapOtter-postgres pg_restore --list < snapotter.dump >/dev/null
# Restore into a fresh database
cat backup.sql | docker exec -i SnapOtter-postgres psql -U snapotter snapotter
# Restore only into a fresh/disposable target first; any SQL error fails the command.
docker exec -i SnapOtter-postgres \
pg_restore --exit-on-error --clean --if-exists --no-owner \
-U snapotter -d snapotter < snapotter.dump
```
或者,停止堆疊並對 `SnapOtter-pgdata` 磁碟區建立快照:
透過將每個備份還原到隔離堆疊、檢查資料庫記錄和檔案校驗和以及啟動應用程式來測試每個備份。儲存庫的 `tests/qa/backup-restore-drill.sh` 會針對明確 `QA_IMAGE` 自動執行該發佈閘。
如果您的平台採用崩潰一致的磁碟區快照,請先停止整個堆疊,並將所有關鍵磁碟區快照為一組。來自正在運行的容器的原始 PostgreSQL 資料目錄副本不是受支援的邏輯備份。
### 檔案與佇列備份 {#file-and-queue-backup}
在捕獲文件和隊列卷之前暫停應用程式。使用 `docker inspect` 解析實際磁碟區名稱,強制 Redis 保留其目前狀態,並在保留所有權和權限的情況下進行歸檔:
```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 .
docker stop SnapOtter
docker exec SnapOtter-redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning SAVE
docker stop SnapOtter-redis
DATA_VOLUME="$(docker inspect SnapOtter --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')"
REDIS_VOLUME="$(docker inspect SnapOtter-redis --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')"
install -d -m 700 backup
docker run --rm -v "$DATA_VOLUME:/source:ro" -v "$PWD/backup:/backup" \
alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce tar czf /backup/snapotter-data.tar.gz -C /source .
docker run --rm -v "$REDIS_VOLUME:/source:ro" -v "$PWD/backup:/backup" \
alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce tar czf /backup/snapotter-redis.tar.gz -C /source .
sha256sum backup/snapotter-*.tar.gz > backup/SHA256SUMS
```
### 使用者檔案備份 {#user-files-backup}
應用前重啟Redis。如果您有意排除 `/data/ai`,請刪除整個 AI 子樹,而不是保留不含模型或虛擬環境的 `installed.json` 記錄。保持備份檔案加密、存取受控,並與執行 SnapOtter 的主機分開。
```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 .
```
## 合規工件 {#compliance-artifacts}
所有套件組的 AI 模型總計最多約 24 GB。由於它們可重新下載,請將 `/data/ai/``/data/venv/` 排除在備份之外以節省空間。只有資料庫與使用者檔案是關鍵的。
每個 SnapOtter 版本都包含以下安全工件:
## 合規產出物 {#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 執行記錄 |
| 釋放主體綁定 | 規範 JSON + GitHub 證明 | [GitHub發布](https://github.com/snapotter-hq/SnapOtter/releases)資產:`snapotter-v{version}-release-subjects.json` |
| 歸檔 SBOM | CycloneDX 和 SPDX JSON | 釋放資產:`snapotter-v{version}-archive-linux-{arch}-sbom.{cdx,spdx}.json` |
| 圖片 SBOM | CycloneDX 和 SPDX JSON | 釋放資產:`snapotter-v{version}-image-linux-{arch}-sbom.{cdx,spdx}.json` |
| 漏洞掃描 | Trivy JSON | 發布具有匹配 `archive-linux-{arch}``image-linux-{arch}` 前綴的資產 |
| 漏洞掃描 | SARIF | [GitHub 安全性](https://github.com/snapotter-hq/SnapOtter/security) 選項卡 |
| 靜態分析 | CodeQL (JS/TS + Python) | [GitHub 安全](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 |
| 依賴項更新 | Dependabot | npm、pip、Docker、Actions 的自動每週 PR |
**執行自己的掃描**
**執行自己的掃描:**
從發行中下載 SBOM,並用你偏好的工具掃描它
下載發布主題清單並驗證它是否已由發布工作流程證明
```bash
gh attestation verify snapotter-v2.1.0-release-subjects.json \
--repo snapotter-hq/SnapOtter \
--signer-workflow snapotter-hq/SnapOtter/.github/workflows/release.yml
```
清單中分別記錄了 `releaseTag``releaseCommit``workflowTriggerCommit`。驗證 `releaseCommit` 是否為從不可變標記中剝離的提交,然後驗證存檔、映像、SBOM 的 SHA-256 摘要,或根據 `subjects` 中的條目進行掃描。這種區別是有意為之的:簽出新建立的發布提交不會更改工作流程的 OIDC 憑證中的提交標識。
您也可以直接掃描下載的 SBOM 或影像:
```bash
# Scan with Grype using the CycloneDX SBOM
grype sbom:snapotter-v1.17.2-sbom.cdx.json
grype sbom:snapotter-v2.1.0-image-linux-amd64-sbom.cdx.json
# Scan with Trivy using the SPDX SBOM
trivy sbom snapotter-v1.17.2-sbom.spdx.json
trivy sbom snapotter-v2.1.0-image-linux-amd64-sbom.spdx.json
# Scan the Docker image directly
trivy image snapotter/snapotter:1.17.2
trivy image snapotter/snapotter:2.1.0
```
::: info
SBOM 與漏洞掃描反映的是該發行所發布的確切映像檔。部署後安裝的 AI 模型套件組不包含在 SBOM 中,因為它們是在執行期間下載的。
::: info
影像 SBOMs 和掃描反映了針對該版本發布的具體架構特定影像。檔案 SBOMs 和掃描分別描述預建存檔。部署後安裝的 AI 模型不包含在這些 SBOMs 中,因為它們是在執行下載的。
:::
+5 -1
View File
@@ -11,7 +11,7 @@ SnapOtter 可處理五種模態的檔案:影像、video、audio、PDF 與檔
## 影像格式 {#image-formats}
SnapOtter 支援 55+ 種影像格式的輸入,以及 13 種格式的輸出。
SnapOtter 支援 55+ 種影像格式的輸入,以及 17 種格式的輸出。
## 輸入格式 {#input-formats}
@@ -121,6 +121,10 @@ SnapOtter 支援 55+ 種影像格式的輸入,以及 13 種格式的輸出。
| ICO | ImageMagick CLI | 無損 | Convert 工具 |
| JP2 | opj_compress CLI | 壓縮比 | Convert 工具 |
| QOI | 內嵌編解碼器 | 無損 | Convert 工具 |
| PSD | ImageMagick CLI | 無損 | Convert 工具 |
| PPM | ImageMagick CLI | 無損 | Convert 工具 |
| EPS | ImageMagick CLI | 無損 | Convert 工具 |
| TGA | ImageMagick CLI | 無損 | Convert 工具 |
## Video 格式 {#video-formats}
+13 -10
View File
@@ -1,8 +1,9 @@
---
description: "在 SnapOtter 中管理使用者、內建與自訂角色、權限、API 金鑰、團隊、工作階段以及稽核日誌。"
i18n_source_hash: 5e28af686c96
i18n_source_hash: bea8955f3aff
i18n_provenance: human
i18n_output_hash: 94865da24af7
i18n_output_hash: 60d6d4d6367d
i18n_hash_version: 2
---
# 使用者、角色與權限 {#users-roles-permissions}
@@ -82,12 +83,12 @@ SnapOtter 包含三種內建角色。它們無法被修改或刪除。
| `pipelines:all` | 檢視並管理所有使用者的管線 |
| `settings:read` | 檢視執行個體設定 |
| `settings:write` | 修改執行個體設定 |
| `users:manage` | 建立、更新並刪除使用者帳 |
| `users:manage` | 在參與者的權限範圍內建立和管理使用者帳 |
| `teams:manage` | 建立、更新並刪除團隊 |
| `features:manage` | 安裝並管理 AI 功能套組 |
| `system:health` | 存取健康檢查與就緒狀態端點 |
| `audit:read` | 檢視稽核日誌並列出角色 |
| `compliance:manage` | 管理 GDPR 生命週期合規功能 |
| `compliance:manage` | 管理 GDPR 生命週期合規功能;破壞性使用者操作仍受權限限制 |
| `webhooks:manage` | 設定對外 webhook |
| `security:manage` | 管理安全性設定(IP 允許清單、SSO 強制執行) |
@@ -110,15 +111,17 @@ curl -X POST http://localhost:1349/api/v1/roles \
角色名稱必須為 2 至 30 個字元,小寫英數字並可含連字號與底線。
### 保留給管理員的權限 {#admin-reserved-permissions}
### 委派管理邊界 {#delegated-administration-boundaries}
有三項權限保留給內建角色,無法指派給自訂角色
所有 17 個權限都可以透過自訂角色委派,但管理權限並不會使該角色等同於內建 `admin` 角色。 `users:manage`授權的使用者突變、`compliance:manage`授權的破壞性操作以及`security:manage`授權的自訂角色管理均受參與者目前權限的約束
- `compliance:manage`
- `webhooks:manage`
- `security:manage`
- 內建角色遵循`admin` > `editor` > `user`;自訂角色位於內建角色下方。
- 目標的權限必須包含在參與者的**有效**權限中。因此,限定範圍的 API 金鑰無法行使其範圍中省略的權限。
- 目標角色的工具存取權限必須包含在參與者自己的工具存取權限中。
- 當角色記錄為 `disabled:<original-role>` 時,已停用的帳戶將根據其原始角色進行檢查。
- 刪除自訂角色也需要指派內建`user`後備的權限;已停用的成員仍停用為 `disabled:user`
角色 API 會拒絕任何包含這些權限的請求。只有內建 `admin` 角色能存取它們
全域憑證和設定更加嚴格:頒發或撤銷 SCIM 令牌以及匯入實例配置需要具有完整有效管理權限的內建 `admin` 角色。
### 工具層級權限 {#tool-level-permissions}