mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(docs-i18n): translate all documentation into 20 languages
All 181 docs markdown files translated into 20 languages (apps/docs/<locale>/**). Companion to the i18n code PR; admin-merged because the file count exceeds GitHub's per-PR CI trigger limit. Validated by pnpm i18n:check (all surfaces, 0 stale/missing) and a clean all-locale docs build.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
---
|
||||
description: "โครงสร้าง monorepo, สถาปัตยกรรมของแอปและแพ็กเกจ, วงจรชีวิตของคำขอ และรอยเท้าทรัพยากรของ SnapOtter"
|
||||
i18n_source_hash: 9e8f80499a37
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: d1d73eea741b
|
||||
---
|
||||
|
||||
# Architecture {#architecture}
|
||||
|
||||
SnapOtter เป็น monorepo ที่จัดการด้วย pnpm workspaces และ Turborepo ปรับใช้เป็นสแตก Docker Compose 3 คอนเทนเนอร์: อิมเมจแอป SnapOtter, PostgreSQL 17 และ Redis 8
|
||||
|
||||
## Project structure {#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 {#packages}
|
||||
|
||||
### `@snapotter/image-engine` {#snapotter-image-engine}
|
||||
|
||||
ไลบรารีประมวลผลรูปภาพหลักที่สร้างบน [Sharp](https://sharp.pixelplumbing.com/) จัดการการดำเนินการที่ไม่ใช่ AI ทั้งหมด: ปรับขนาด, ครอบตัด, หมุน, พลิก, แปลง, บีบอัด, ลบเมทาดาทา และปรับสี (ความสว่าง, ความเปรียบต่าง, ความอิ่มตัว, ขาวดำ, ซีเปีย, กลับสี, ช่องสี)
|
||||
|
||||
แพ็กเกจนี้ไม่มี dependency ของเครือข่ายและทำงานภายในกระบวนการทั้งหมด
|
||||
|
||||
### `@snapotter/ai` {#snapotter-ai}
|
||||
|
||||
เลเยอร์เชื่อมต่อที่เรียกสคริปต์ Python สำหรับการดำเนินการ ML เมื่อใช้งานครั้งแรก บริดจ์จะเริ่มกระบวนการ Python dispatcher ที่คงอยู่ ซึ่งนำเข้าไลบรารีหนัก (PIL, NumPy, MediaPipe, rembg) ไว้ล่วงหน้า เพื่อให้การเรียก AI ครั้งต่อ ๆ ไปข้ามค่าใช้จ่ายการนำเข้า หาก dispatcher ยังไม่พร้อม บริดจ์จะย้อนกลับไปสร้าง subprocess ของ Python ใหม่ต่อคำขอ
|
||||
|
||||
**โมเดลไม่ได้ถูกโหลดล่วงหน้า** สคริปต์ของแต่ละเครื่องมือโหลดน้ำหนักโมเดลจากดิสก์ ณ เวลาที่ขอ และทิ้งเมื่อคำขอเสร็จสิ้น ดู [Resource footprint](#resource-footprint) สำหรับโปรไฟล์หน่วยความจำทั้งหมด
|
||||
|
||||
การดำเนินการที่รองรับ: การลบพื้นหลัง (rembg/BiRefNet), การขยายภาพ (RealESRGAN), การเบลอใบหน้า (MediaPipe), การเพิ่มความคมชัดใบหน้า (GFPGAN/CodeFormer), การลบวัตถุ (LaMa ONNX), OCR (PaddleOCR/Tesseract), การลงสี (DDColor), การลบสัญญาณรบกวน, การลบตาแดง, การฟื้นฟูภาพถ่าย, การสร้างรูปถ่ายหนังสือเดินทาง, การแก้ความโปร่งใส (BiRefNet HR-matting) และการปรับขนาดที่คำนึงถึงเนื้อหา (ไบนารี Go caire)
|
||||
|
||||
สคริปต์ Python อยู่ใน `packages/ai/python/` อิมเมจ Docker ดาวน์โหลดน้ำหนักโมเดลทั้งหมดล่วงหน้าระหว่างการสร้าง เพื่อให้คอนเทนเนอร์ทำงานได้อย่างสมบูรณ์แบบออฟไลน์
|
||||
|
||||
### `@snapotter/shared` {#snapotter-shared}
|
||||
|
||||
ประเภท TypeScript ที่ใช้ร่วมกัน, ค่าคงที่ (เช่น `APP_VERSION` และการกำหนดเครื่องมือ) และสตริงการแปล i18n ที่ใช้ทั้งส่วนหน้าและส่วนหลัง
|
||||
|
||||
## Applications {#applications}
|
||||
|
||||
### API (`apps/api`) {#api-apps-api}
|
||||
|
||||
เซิร์ฟเวอร์ Fastify v5 ที่เปิดเผยเส้นทางเครื่องมือ 241 เส้นทางครอบคลุมห้ารูปแบบ (image, video, audio, PDF, file) ที่จัดการ:
|
||||
- การอัปโหลดไฟล์, การจัดการพื้นที่ทำงานชั่วคราว และที่จัดเก็บไฟล์แบบถาวร
|
||||
- คลังไฟล์ผู้ใช้พร้อมห่วงโซ่เวอร์ชัน (ตาราง `user_files`) - ผลลัพธ์ที่ประมวลผลแล้วแต่ละรายการเชื่อมโยงกลับไปยังไฟล์ต้นทางและบันทึกว่าใช้เครื่องมือใด พร้อมภาพขนาดย่อที่สร้างอัตโนมัติสำหรับหน้า Files
|
||||
- การเรียกใช้เครื่องมือ (กำหนดเส้นทางคำขอเครื่องมือแต่ละรายการไปยังเอนจินรูปภาพหรือบริดจ์ AI)
|
||||
- การประสานงานไปป์ไลน์ (เชื่อมโยงเครื่องมือหลายตัวตามลำดับ)
|
||||
- การประมวลผลเป็นชุดพร้อมการควบคุมการทำงานพร้อมกันผ่านคิวงาน BullMQ (pool: image, media, ai, docs, system)
|
||||
- การยืนยันตัวตนผู้ใช้, RBAC (บทบาท admin/user พร้อมชุดสิทธิ์เต็ม), การจัดการคีย์ API และการจำกัดอัตรา
|
||||
- การจัดการทีม - CRUD เฉพาะ admin ผู้ใช้ถูกกำหนดให้อยู่ในทีมผ่านฟิลด์ `team` บนโปรไฟล์ของพวกเขา
|
||||
- การตั้งค่ารันไทม์ - ที่จัดเก็บแบบคีย์-ค่าในตาราง `settings` ที่ควบคุม `disabledTools`, `enableExperimentalTools`, `loginAttemptLimit` และปุ่มปรับการทำงานอื่น ๆ โดยไม่ต้องปรับใช้ใหม่
|
||||
- การสร้างแบรนด์กำหนดเองและการตั้งค่ารันไทม์ผ่านการตั้งค่าที่รองรับด้วยฐานข้อมูล
|
||||
- เอกสาร Scalar/OpenAPI ที่ `/api/docs`
|
||||
- การเสิร์ฟส่วนหน้าที่สร้างแล้วเป็น SPA ในการใช้งานจริง
|
||||
|
||||
Dependency หลัก: Fastify, Drizzle ORM (pg-core, node-postgres), Sharp, BullMQ, ioredis, Zod สำหรับการตรวจสอบ
|
||||
|
||||
เซิร์ฟเวอร์จัดการการปิดตัวอย่างสง่างามเมื่อได้รับ SIGTERM/SIGINT: มันระบายการเชื่อมต่อ HTTP, หยุด worker ของ BullMQ, ปิด Python dispatcher และปิดการเชื่อมต่อฐานข้อมูล
|
||||
|
||||
### Web (`apps/web`) {#web-apps-web}
|
||||
|
||||
แอปหน้าเดียว React 19 ที่สร้างด้วย Vite ใช้ Zustand สำหรับการจัดการสถานะ, Tailwind CSS v4 สำหรับการจัดสไตล์ และ Lucide สำหรับไอคอน สื่อสารกับ API ผ่าน REST และ SSE (สำหรับการติดตามความคืบหน้า)
|
||||
|
||||
หน้าต่าง ๆ รวมถึงพื้นที่ทำงานเครื่องมือ, หน้า Files สำหรับจัดการการอัปโหลดและผลลัพธ์แบบถาวร, ตัวสร้างระบบอัตโนมัติ/ไปป์ไลน์ และแผงการตั้งค่าผู้ดูแลระบบ
|
||||
|
||||
ส่วนหน้าที่สร้างแล้วถูกเสิร์ฟโดยส่วนหลัง Fastify ในการใช้งานจริง จึงไม่มีเว็บเซิร์ฟเวอร์แยกต่างหากในคอนเทนเนอร์ Docker
|
||||
|
||||
### Docs (`apps/docs`) {#docs-apps-docs}
|
||||
|
||||
ไซต์ VitePress นี้ ปรับใช้ไปยัง Cloudflare Pages โดยอัตโนมัติเมื่อ push ไปยัง `main`
|
||||
|
||||
## How a request flows {#how-a-request-flows}
|
||||
|
||||
1. ผู้ใช้เลือกเครื่องมือใน UI เว็บและอัปโหลดไฟล์
|
||||
2. ส่วนหน้าส่ง multipart POST ไปยัง `/api/v1/tools/:section/:toolId` พร้อมไฟล์และการตั้งค่า
|
||||
3. เส้นทาง API ตรวจสอบอินพุตด้วย Zod จากนั้นส่งต่อการประมวลผล
|
||||
4. สำหรับเครื่องมือมาตรฐาน งานจะถูกจัดคิวไปยัง BullMQ pool ที่เหมาะสม (image, media หรือ docs ตามรูปแบบ) worker BullMQ ในกระบวนการจะปรับทิศทางภาพอัตโนมัติตามเมทาดาทา EXIF, รันฟังก์ชันการประมวลผลของเครื่องมือ และส่งคืนผลลัพธ์
|
||||
5. สำหรับเครื่องมือ AI บริดจ์ TypeScript จะส่งคำขอไปยัง Python dispatcher ที่คงอยู่ (หรือสร้าง subprocess ใหม่เป็นทางเลือกสำรอง) รอให้เสร็จ แล้วอ่านไฟล์เอาต์พุต
|
||||
6. ความคืบหน้าของงานจะถูกบันทึกลงในตาราง `jobs` ใน PostgreSQL เพื่อให้สถานะอยู่รอดจากการรีสตาร์ตคอนเทนเนอร์ การอัปเดตแบบเรียลไทม์ถูกส่งผ่าน SSE ที่ `/api/v1/jobs/:jobId/progress`
|
||||
7. API ส่งคืน `jobId` และ `downloadUrl` ผู้ใช้ดาวน์โหลดไฟล์ที่ประมวลผลแล้วจาก `/api/v1/download/:jobId/:filename`
|
||||
|
||||
สำหรับไปป์ไลน์ API จะป้อนเอาต์พุตของแต่ละขั้นตอนเป็นอินพุตให้ขั้นตอนถัดไป โดยรันตามลำดับ
|
||||
|
||||
สำหรับการประมวลผลเป็นชุด API ใช้ BullMQ flow พร้อม child job ต่อขั้นตอน และส่งคืนไฟล์ ZIP พร้อมไฟล์ที่ประมวลผลแล้วทั้งหมด
|
||||
|
||||
## Resource footprint {#resource-footprint}
|
||||
|
||||
SnapOtter ออกแบบมาเพื่อการใช้หน่วยความจำขณะว่างต่ำ ไม่มีสิ่งใดถูกโหลดล่วงหน้าหรืออุ่นไว้ตอนเริ่มต้น
|
||||
|
||||
### At idle {#at-idle}
|
||||
|
||||
กระบวนการ Node.js/Fastify, PostgreSQL และ Redis กำลังทำงาน RAM ขณะว่างโดยทั่วไปอยู่ที่ **~200-300 MB** รวมทั้งสามคอนเทนเนอร์ (กระบวนการ Node.js, Postgres และ Redis) ไม่มีกระบวนการ Python ไม่มีน้ำหนักโมเดลในหน่วยความจำ
|
||||
|
||||
### What starts, and when {#what-starts-and-when}
|
||||
|
||||
| Component | Starts when | Memory while active |
|
||||
|-----------|-------------|---------------------|
|
||||
| เซิร์ฟเวอร์ Fastify + Postgres + Redis | เมื่อคอนเทนเนอร์เริ่ม | ~200-300 MB รวม |
|
||||
| worker BullMQ | เมื่อคอนเทนเนอร์เริ่ม (ในกระบวนการ) | หนึ่ง worker ต่อ pool (image, media, ai, docs, system) |
|
||||
| Python dispatcher | คำขอเครื่องมือ AI ครั้งแรก | ตัวแปล Python + ไลบรารีที่นำเข้าล่วงหน้า (PIL, NumPy, MediaPipe, rembg) - ไม่มีน้ำหนักโมเดล |
|
||||
| น้ำหนักโมเดล AI | ระหว่างคำขอของเครื่องมือนั้น ๆ | โหลดจากดิสก์ ปล่อยเมื่อคำขอเสร็จสิ้น |
|
||||
|
||||
### Model loading {#model-loading}
|
||||
|
||||
ไฟล์น้ำหนักโมเดลทั้งหมด (รวมหลาย GB) อยู่บนดิสก์ใน `/opt/models/` ตลอดเวลา สคริปต์เครื่องมือ AI แต่ละตัวโหลดเฉพาะโมเดลของตัวเองเข้าหน่วยความจำตลอดระยะเวลาของคำขอ แล้วปล่อยออก บางสคริปต์เรียก `del model` และ `torch.cuda.empty_cache()` อย่างชัดเจนหลังการอนุมานเพื่อให้แน่ใจว่าหน่วยความจำถูกคืนทันที
|
||||
|
||||
ไม่มีแคชโมเดลระหว่างคำขอ การรันเครื่องมือ AI เดียวกันติดต่อกันจะโหลดโมเดลใหม่ทุกครั้ง สิ่งนี้ทำให้หน่วยความจำขณะว่างเข้าใกล้ศูนย์ โดยแลกกับความล่าช้าในการโหลดโมเดลในทุกคำขอ AI
|
||||
|
||||
### First AI request cold start {#first-ai-request-cold-start}
|
||||
|
||||
Python dispatcher ไม่ทำงานเมื่อคอนเทนเนอร์เริ่มต้น คำขอ AI ครั้งแรกกระตุ้นสองสิ่งพร้อมกัน: dispatcher เริ่มอุ่นเครื่องในเบื้องหลัง และคำขอนั้นเองจะย้อนกลับไปสร้าง Python subprocess แบบครั้งเดียว เมื่อ dispatcher ส่งสัญญาณว่าพร้อม คำขอ AI ที่ตามมาทั้งหมดจะใช้มันโดยตรงและข้ามค่าใช้จ่ายในการสร้าง subprocess
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
description: "ตัวแปรสภาพแวดล้อมทั้งหมดของ SnapOtter พร้อมค่าเริ่มต้น กำหนดค่าการยืนยันตัวตน, ที่จัดเก็บ, โมเดล AI, การวิเคราะห์ข้อมูล และอื่น ๆ"
|
||||
i18n_source_hash: 8e9e9ca2840c
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 891bad80245a
|
||||
---
|
||||
|
||||
# Configuration {#configuration}
|
||||
|
||||
การกำหนดค่าทั้งหมดทำผ่านตัวแปรสภาพแวดล้อม ทุกตัวแปรมีค่าเริ่มต้นที่เหมาะสม ดังนั้น SnapOtter จึงทำงานได้ทันทีโดยไม่ต้องตั้งค่าใด ๆ
|
||||
|
||||
## Environment variables {#environment-variables}
|
||||
|
||||
### Server {#server}
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `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` จาก reverse proxy ตั้งเป็น `false` หากไม่ได้อยู่หลัง proxy |
|
||||
|
||||
### Authentication {#authentication}
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `AUTH_ENABLED` | `false` | ตั้งเป็น `true` เพื่อบังคับให้เข้าสู่ระบบ อิมเมจ Docker มีค่าเริ่มต้นเป็น `true` |
|
||||
| `DEFAULT_USERNAME` | `admin` | ชื่อผู้ใช้สำหรับบัญชี admin เริ่มต้น ใช้เฉพาะตอนรันครั้งแรก |
|
||||
| `DEFAULT_PASSWORD` | `admin` | รหัสผ่านสำหรับบัญชี admin เริ่มต้น เปลี่ยนหลังจากเข้าสู่ระบบครั้งแรก |
|
||||
| `MAX_USERS` | `0` (ไม่จำกัด) | จำนวนบัญชีผู้ใช้ที่ลงทะเบียนสูงสุด ตั้งเป็น 0 สำหรับไม่จำกัด |
|
||||
| `SESSION_DURATION_HOURS` | `168` | อายุของ session การเข้าสู่ระบบเป็นชั่วโมง (ค่าเริ่มต้นคือ 7 วัน) |
|
||||
| `SKIP_MUST_CHANGE_PASSWORD` | - | ตั้งเป็นค่าที่ไม่ว่างใด ๆ เพื่อข้ามการแจ้งให้เปลี่ยนรหัสผ่านแบบบังคับตอนเข้าสู่ระบบครั้งแรก |
|
||||
|
||||
### Storage {#storage}
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `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 {#embedded-mode}
|
||||
|
||||
รันอิมเมจโดยไม่มี `DATABASE_URL` และไม่มี `REDIS_URL` แล้วมันจะเริ่ม PostgreSQL 17 และ Redis ของตัวเองภายในคอนเทนเนอร์ ผูกกับ loopback โดยข้อมูลทั้งหมดอยู่บนวอลุ่ม `/data` สิ่งนี้ฟื้นฟูประสบการณ์ `docker run` ด้วยคำสั่งเดียวสำหรับการเริ่มต้นอย่างรวดเร็ว, homelab และการอัปเกรดจาก 1.x เป็นเส้นทางเพื่อความสะดวก ไม่ใช่การปรับใช้เพื่อการใช้งานจริง: สำหรับการใช้งานจริง ให้รันสแตก Compose 3 คอนเทนเนอร์พร้อม PostgreSQL และ Redis แยกต่างหาก โหมด embedded ต้องรันคอนเทนเนอร์เป็น root และเข้ากันไม่ได้กับรันไทม์ที่ใช้ UID ตามอำเภอใจ (OpenShift, Kubernetes `runAsNonRoot`) ให้ใช้ Compose ที่นั่น
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `EMBEDDED` | `auto` | เปิดใช้อัตโนมัติเมื่อทั้ง `DATABASE_URL` และ `REDIS_URL` ไม่ได้ตั้งค่า ตั้งเป็น `0` เพื่อปิด (แอปจะล้มเหลวอย่างรวดเร็วหากไม่มี `DATABASE_URL`/`REDIS_URL` ภายนอกที่ตั้งค่าไว้ แทนที่จะเริ่มฐานข้อมูลในคอนเทนเนอร์อย่างเงียบ ๆ) |
|
||||
| `REDIS_MAXMEMORY` | `512mb` | ขีดจำกัดหน่วยความจำสำหรับ Redis แบบฝังในตัว (เฉพาะโหมด embedded) ลดค่านี้บนโฮสต์ที่มีหน่วยความจำจำกัด เช่น Raspberry Pi |
|
||||
|
||||
การอัปเกรดจาก 1.x: วาง `snapotter.db` เก่าของคุณไว้ที่ `/data/snapotter.db` ในวอลุ่ม แล้วโหมด embedded จะนำเข้าไปยัง PostgreSQL แบบฝังในตัวเมื่อบูตครั้งแรก การนำเข้าทำงานครั้งเดียว การบูตครั้งต่อ ๆ ไปจะข้ามมัน
|
||||
|
||||
หมายเหตุเกี่ยวกับ telemetry: โหมด embedded สืบทอดค่าเริ่มต้นการวิเคราะห์ข้อมูลของอิมเมจเหมือนการกำหนดค่าอื่น ๆ อิมเมจที่เผยแพร่มาพร้อมการวิเคราะห์ข้อมูลที่เปิดอยู่ สร้างด้วย `--build-arg SNAPOTTER_ANALYTICS=off` หรือใช้การเลือกไม่เข้าร่วมของ admin ในแอป เพื่อปิดใช้งาน
|
||||
|
||||
### Processing limits {#processing-limits}
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `MAX_UPLOAD_SIZE_MB` | `100` | ขนาดไฟล์สูงสุดต่อการอัปโหลดเป็นเมกะไบต์ ตั้งเป็น 0 สำหรับไม่จำกัด |
|
||||
| `MAX_BATCH_SIZE` | `100` | จำนวนไฟล์สูงสุดในคำขอชุดเดียว ตั้งเป็น 0 สำหรับไม่จำกัด |
|
||||
| `CONCURRENT_JOBS` | `0` (อัตโนมัติ) | จำนวนงานชุดที่รันแบบขนาน ตั้งเป็น 0 เพื่อตรวจจับอัตโนมัติตามแกน CPU ที่มีอยู่ |
|
||||
| `MAX_MEGAPIXELS` | `0` (ไม่จำกัด) | ความละเอียดภาพสูงสุดที่อนุญาตเป็นเมกะพิกเซล ตั้งเป็น 0 สำหรับไม่จำกัด |
|
||||
| `MAX_WORKER_THREADS` | `0` (อัตโนมัติ) | เธรด worker สูงสุดสำหรับการประมวลผลรูปภาพ ตั้งเป็น 0 เพื่อตรวจจับอัตโนมัติตามแกน CPU ที่มีอยู่ |
|
||||
| `PROCESSING_TIMEOUT_S` | `0` (ไม่มีขีดจำกัด) | เวลาการประมวลผลสูงสุดต่อคำขอเป็นวินาที ตั้งเป็น 0 สำหรับไม่มีการหมดเวลา |
|
||||
| `MAX_PIPELINE_STEPS` | `20` | จำนวนขั้นตอนสูงสุดในไปป์ไลน์ ตั้งเป็น 0 สำหรับไม่มีขีดจำกัด |
|
||||
| `MAX_CANVAS_PIXELS` | `0` (ไม่มีขีดจำกัด) | ขนาดแคนวาสสูงสุดเป็นพิกเซลสำหรับภาพเอาต์พุต ตั้งเป็น 0 สำหรับไม่มีขีดจำกัด |
|
||||
| `MAX_SVG_SIZE_MB` | `0` (ไม่จำกัด) | ขนาดไฟล์ SVG สูงสุดเป็นเมกะไบต์ ตั้งเป็น 0 สำหรับไม่จำกัด |
|
||||
| `MAX_SPLIT_GRID` | `100` | มิติตารางสูงสุดสำหรับเครื่องมือแยกภาพ |
|
||||
| `MAX_PDF_PAGES` | `0` (ไม่จำกัด) | จำนวนหน้า PDF สูงสุดสำหรับการแปลง PDF-to-image ตั้งเป็น 0 สำหรับไม่จำกัด |
|
||||
|
||||
### Cleanup {#cleanup}
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `FILE_MAX_AGE_HOURS` | `72` | ระยะเวลาที่เก็บผลลัพธ์การประมวลผลที่ไม่ได้บันทึก (การอัปโหลดดิบและเอาต์พุตเครื่องมือ) ก่อนการลบอัตโนมัติ ไฟล์ที่คุณบันทึกลงคลัง Files อย่างชัดเจนจะไม่ได้รับผลกระทบและคงอยู่จนกว่าคุณจะลบ |
|
||||
| `CLEANUP_INTERVAL_MINUTES` | `60` | ความถี่ที่งานล้างข้อมูลทำงาน |
|
||||
|
||||
### Appearance {#appearance}
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `DEFAULT_THEME` | `light` | ธีมเริ่มต้นสำหรับ session ใหม่ `light` หรือ `dark` |
|
||||
| `DEFAULT_LOCALE` | `en` | ภาษาอินเทอร์เฟซเริ่มต้น |
|
||||
| `DEFAULT_TOOL_VIEW` | `sidebar` | เลย์เอาต์เครื่องมือเริ่มต้น `sidebar` หรือ `fullscreen` |
|
||||
|
||||
### Docker permissions {#docker-permissions}
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `PUID` | `999` | รันกระบวนการคอนเทนเนอร์เป็น UID นี้ ตั้งให้ตรงกับผู้ใช้โฮสต์ของคุณสำหรับ bind mount (`id -u`) |
|
||||
| `PGID` | `999` | รันกระบวนการคอนเทนเนอร์เป็น GID นี้ ตั้งให้ตรงกับกลุ่มโฮสต์ของคุณสำหรับ bind mount (`id -g`) |
|
||||
|
||||
## Docker example {#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 {#volumes}
|
||||
|
||||
สแตก Docker Compose ใช้สี่วอลุ่ม:
|
||||
|
||||
- `/data` (app) - โมเดล AI, Python venv และไฟล์ผู้ใช้ เมานต์นี้เพื่อเก็บไฟล์ที่อัปโหลดและชุด AI ที่ติดตั้งไว้ข้ามการรีสตาร์ต
|
||||
- `/tmp/workspace` (app) - ที่จัดเก็บชั่วคราวสำหรับไฟล์ที่กำลังประมวลผล อาจเป็นแบบชั่วคราวได้ แต่การเมานต์ช่วยหลีกเลี่ยงการเติมเลเยอร์ที่เขียนได้ของคอนเทนเนอร์
|
||||
- `SnapOtter-pgdata` (postgres) - ไดเรกทอรีข้อมูล PostgreSQL เก็บข้อมูลเชิงสัมพันธ์ทั้งหมด (users, settings, pipelines, jobs, audit log) สำรองข้อมูลผ่าน `pg_dump` หรือ snapshot ของวอลุ่ม
|
||||
- `SnapOtter-redisdata` (redis) - ไฟล์ append-only ของ Redis สำหรับคิวงานที่คงทน
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
description: "วิธีมีส่วนร่วมกับ SnapOtter รายงานบั๊ก คำขอฟีเจอร์ pull request และข้อกำหนดเรื่อง CLA"
|
||||
i18n_source_hash: 528802503035
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 33d63a5f1932
|
||||
---
|
||||
|
||||
# การมีส่วนร่วม {#contributing}
|
||||
|
||||
ขอบคุณที่สนใจมีส่วนร่วม คู่มือนี้อธิบายว่าจะเข้าร่วมได้อย่างไร เรารับอะไรบ้าง และจะเริ่มต้นอย่างไร
|
||||
|
||||
## วิธีมีส่วนร่วม {#ways-to-contribute}
|
||||
|
||||
### Issue (ไม่ต้องตั้งค่าอะไร) {#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) เพื่อให้ชุมชนได้แสดงความเห็นและโหวตสนับสนุน
|
||||
- **ปัญหาเรื่องการแปล** - เจอคำแปลที่ผิดหรือขาดหายไหม? เปิด [issue เรื่องการแปล](https://github.com/snapotter-hq/snapotter/issues/new?template=translation.yml)
|
||||
- **ปัญหาเรื่องเอกสาร** - มีอะไรผิดพลาดในเอกสารไหม? เปิด [issue เรื่องเอกสาร](https://github.com/snapotter-hq/snapotter/issues/new?template=documentation.yml)
|
||||
|
||||
### โค้ด (ต้องมี CLA) {#code-requires-cla}
|
||||
|
||||
เรารับ pull request สำหรับ:
|
||||
|
||||
| ประเภท | ขั้นตอน |
|
||||
|------|---------|
|
||||
| การแก้บั๊ก | เปิด PR ได้เลย (ลิงก์ไปยัง issue ถ้ามี) |
|
||||
| การแปลใหม่ | เปิด PR ได้เลย (ดู [คู่มือการแปล](/th/guide/translations)) |
|
||||
| การปรับปรุงเอกสาร | เปิด PR ได้เลย |
|
||||
| การปรับปรุงความครอบคลุมของเทสต์ | เปิด PR ได้เลย |
|
||||
| เครื่องมือหรือฟีเจอร์ใหม่ | เริ่ม [การสนทนา](https://github.com/snapotter-hq/snapotter/discussions/new?category=ideas) ก่อน ผู้ดูแลจะแปลงไอเดียที่อนุมัติแล้วเป็น issue ที่ติดตามได้ก่อนที่คุณจะเขียนโค้ด |
|
||||
| การ refactor หรือเปลี่ยนสถาปัตยกรรม | เริ่ม [การสนทนา](https://github.com/snapotter-hq/snapotter/discussions/new?category=ideas) ก่อนและรอให้ผู้ดูแลอนุมัติก่อนเขียนโค้ด |
|
||||
|
||||
### สิ่งที่เราจะไม่รับ {#what-we-will-not-accept}
|
||||
|
||||
- การเปลี่ยนแปลง workflow ของ CI/CD, การตั้งค่า release หรือการตั้งค่า linter/compiler
|
||||
- PR ที่ไม่มีการลงนาม [Contributor License Agreement](#contributor-license-agreement)
|
||||
- PR ที่มีการเปลี่ยนแปลงเกิน 400 บรรทัด (แบ่งงานใหญ่ออกเป็น PR ย่อย)
|
||||
- ฟีเจอร์ที่ไม่ได้ถูกอภิปรายและอนุมัติก่อน
|
||||
- การเปลี่ยนแปลง `packages/ai/` โดยไม่มีการอภิปรายก่อน
|
||||
|
||||
## Contributor License Agreement {#contributor-license-agreement}
|
||||
|
||||
ก่อนที่เราจะรวม PR แรกของคุณได้ คุณต้องลงนาม [Individual CLA](https://github.com/snapotter-hq/snapotter/blob/main/CLA.md) ของเรา นี่เป็นข้อกำหนดที่ทำเพียงครั้งเดียว
|
||||
|
||||
**ทำไม:** SnapOtter มีสัญญาอนุญาตแบบคู่ (AGPLv3 + commercial) CLA ให้สิทธิ์เราในการแจกจ่ายการมีส่วนร่วมของคุณภายใต้สัญญาอนุญาตทั้งสองแบบ คุณยังคงเป็นเจ้าของลิขสิทธิ์เต็มรูปแบบในผลงานของคุณ
|
||||
|
||||
**อย่างไร:** เมื่อคุณเปิด PR แรก บอท CLA Assistant จะแสดงความคิดเห็นพร้อมลิงก์ คลิกลิงก์ ทบทวนข้อตกลง และลงนามด้วยบัญชี GitHub ของคุณ ใช้เวลา 30 วินาที
|
||||
|
||||
หากคุณมีส่วนร่วมในนามของนายจ้างและนายจ้างของคุณคงไว้ซึ่งสิทธิ์ในทรัพย์สินทางปัญญาเหนือผลงานของคุณ กรุณาติดต่อ contact@snapotter.com เพื่อจัดทำ Corporate CLA ก่อนส่ง
|
||||
|
||||
## เริ่มต้นใช้งาน {#getting-started}
|
||||
|
||||
### สิ่งที่ต้องมีก่อน {#prerequisites}
|
||||
|
||||
- Node.js 22+
|
||||
- pnpm 9+
|
||||
- Python 3.11+ (สำหรับเครื่องมือ AI เท่านั้น)
|
||||
- Docker (ไม่บังคับ สำหรับการทดสอบ integration เต็มรูปแบบ)
|
||||
|
||||
### การตั้งค่า {#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 {#pull-request-process}
|
||||
|
||||
1. Fork repo และสร้าง branch จาก `main` (`feat/my-feature` หรือ `fix/issue-123`)
|
||||
2. ทำการเปลี่ยนแปลงของคุณในคอมมิตที่โฟกัสและตรวจสอบได้ง่ายโดยใช้ [conventional commits](https://www.conventionalcommits.org/)
|
||||
3. เพิ่มหรืออัปเดตเทสต์สำหรับการเปลี่ยนแปลงของคุณ
|
||||
4. รัน `pnpm lint && pnpm typecheck && pnpm test` ในเครื่อง
|
||||
5. เปิด PR เทียบกับ `main` และกรอกแม่แบบ
|
||||
6. ลงนาม CLA หากได้รับแจ้ง
|
||||
7. รอให้ CI ผ่านและผู้ดูแลตรวจทาน
|
||||
|
||||
### สิ่งที่คาดหวังจากการตรวจทาน {#review-expectations}
|
||||
|
||||
- เราตั้งเป้าจะตอบ PR ภายใน 7 วัน
|
||||
- PR ที่เล็กและโฟกัสจะได้รับการตรวจทานเร็วกว่า
|
||||
- หากไม่ได้รับการตอบกลับภายใน 7 วัน ให้แสดงความคิดเห็นเพื่อเตือนในเธรด
|
||||
- เราอาจขอให้เปลี่ยนแปลง แนะนำแนวทางอื่น หรือปิด PR หากไม่สอดคล้องกับทิศทางของโปรเจกต์
|
||||
|
||||
### หลังจาก PR ของคุณถูกรวมแล้ว {#after-your-pr-is-merged}
|
||||
|
||||
การมีส่วนร่วมของคุณจะถูกรวมไว้ในการรีลีสถัดไปและได้รับเครดิตใน changelog
|
||||
|
||||
## Issue ที่เหมาะกับผู้เริ่มต้น {#good-first-issues}
|
||||
|
||||
กำลังมองหางานทำอยู่ไหม? ดู [good first issues](https://github.com/snapotter-hq/snapotter/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) ของเราสำหรับงานที่เหมาะกับผู้เริ่มต้น หรือ [help wanted](https://github.com/snapotter-hq/snapotter/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) สำหรับงานใหญ่ที่เรายินดีรับความช่วยเหลือจากชุมชน
|
||||
|
||||
## รูปแบบโค้ด {#code-style}
|
||||
|
||||
- Biome จัดการการฟอร์แมตและ linting (double quotes, semicolons, การเยื้อง 2 ช่องว่าง)
|
||||
- Pre-commit hook จะรัน `biome check --write` กับไฟล์ที่ staged โดยอัตโนมัติ
|
||||
- หาก linter ร้องเรียน ให้แก้ไขโค้ด (อย่าแก้ไขการตั้งค่า Biome)
|
||||
- ES modules ทุกที่ (`import`/`export`)
|
||||
- Conventional commits: `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`
|
||||
|
||||
สำหรับรายละเอียดสถาปัตยกรรมทั้งหมด ดู [คู่มือนักพัฒนา](/th/guide/developer)
|
||||
|
||||
## ความปลอดภัย {#security}
|
||||
|
||||
**อย่าเปิด PR หรือ issue สาธารณะสำหรับช่องโหว่ด้านความปลอดภัย** รายงานเป็นการส่วนตัวผ่าน [GitHub Security Advisories](https://github.com/snapotter-hq/snapotter/security/advisories/new) หรืออีเมล contact@snapotter.com ดู [SECURITY.md](https://github.com/snapotter-hq/snapotter/blob/main/SECURITY.md) สำหรับรายละเอียดทั้งหมด
|
||||
|
||||
## มีคำถาม? {#questions}
|
||||
|
||||
- [เอกสาร](https://docs.snapotter.com/)
|
||||
- [Discord](https://discord.gg/hr3s7HPUsr)
|
||||
- [GitHub Discussions](https://github.com/snapotter-hq/snapotter/discussions)
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
description: "สคีมาฐานข้อมูล PostgreSQL ตาราง การย้ายข้อมูล และขั้นตอนการสำรองข้อมูลสำหรับ SnapOtter"
|
||||
i18n_source_hash: b37398ae91a3
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: b589c2175a16
|
||||
---
|
||||
|
||||
# ฐานข้อมูล {#database}
|
||||
|
||||
SnapOtter ใช้ PostgreSQL 17 กับ [Drizzle ORM](https://orm.drizzle.team/) (pg-core / node-postgres) สำหรับการจัดเก็บข้อมูลอย่างถาวร สคีมาถูกกำหนดไว้ใน `apps/api/src/db/schema.ts`
|
||||
|
||||
การเชื่อมต่อถูกกำหนดค่าผ่านตัวแปรสภาพแวดล้อม `DATABASE_URL` (ค่าเริ่มต้น `postgres://snapotter:snapotter@postgres:5432/snapotter`) ใน Docker Compose คอนเทนเนอร์ Postgres จะเก็บข้อมูลไว้ใน named volume `SnapOtter-pgdata`
|
||||
|
||||
## ตาราง {#tables}
|
||||
|
||||
### users {#users}
|
||||
|
||||
เก็บบัญชีผู้ใช้ สร้างขึ้นโดยอัตโนมัติเมื่อรันครั้งแรกจาก `DEFAULT_USERNAME` และ `DEFAULT_PASSWORD`
|
||||
|
||||
| คอลัมน์ | ชนิด | หมายเหตุ |
|
||||
|---|---|---|
|
||||
| `id` | uuid | คีย์หลัก |
|
||||
| `username` | varchar | ไม่ซ้ำ จำเป็น |
|
||||
| `passwordHash` | varchar | scrypt hash |
|
||||
| `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 สำหรับการเข้าถึงแบบโปรแกรม คีย์ดิบจะแสดงเพียงครั้งเดียวตอนสร้าง เก็บเฉพาะ hash เท่านั้น
|
||||
|
||||
| คอลัมน์ | ชนิด | หมายเหตุ |
|
||||
|---|---|---|
|
||||
| `id` | uuid | คีย์หลัก |
|
||||
| `userId` | uuid | คีย์นอกไปยัง `users.id` |
|
||||
| `keyHash` | varchar | scrypt hash ของคีย์ |
|
||||
| `name` | varchar | ป้ายชื่อที่ผู้ใช้กำหนด |
|
||||
| `createdAt` | timestamp | เวลาที่สร้าง |
|
||||
| `lastUsedAt` | timestamp | อัปเดตทุกครั้งที่มีการร้องขอที่ผ่านการยืนยันตัวตน |
|
||||
|
||||
คีย์จะขึ้นต้นด้วย `si_` ตามด้วยอักขระเลขฐานสิบหก 96 ตัว (สุ่ม 48 ไบต์)
|
||||
|
||||
### pipelines {#pipelines}
|
||||
|
||||
ชุดเครื่องมือที่บันทึกไว้ซึ่งผู้ใช้สร้างขึ้นใน UI
|
||||
|
||||
| คอลัมน์ | ชนิด | หมายเหตุ |
|
||||
|---|---|---|
|
||||
| `id` | uuid | คีย์หลัก |
|
||||
| `name` | varchar | ชื่อ pipeline |
|
||||
| `description` | varchar | คำอธิบายที่ไม่บังคับ |
|
||||
| `steps` | jsonb | อาร์เรย์ของอ็อบเจกต์ `{ toolId, settings }` |
|
||||
| `createdAt` | timestamp | เวลาที่สร้าง |
|
||||
|
||||
### user_files {#user-files}
|
||||
|
||||
คลังไฟล์ถาวรพร้อมการติดตามสายเวอร์ชัน แต่ละขั้นตอนการประมวลผลที่บันทึกผลลัพธ์จะสร้างแถวใหม่ที่เชื่อมโยงกับแถวแม่ผ่าน `parentId` กลายเป็นต้นไม้เวอร์ชัน
|
||||
|
||||
| คอลัมน์ | ชนิด | คำอธิบาย |
|
||||
|--------|------|-------------|
|
||||
| `id` | uuid | คีย์หลัก |
|
||||
| `userId` | uuid | FK ไปยัง users (CASCADE DELETE) |
|
||||
| `originalName` | varchar | ชื่อไฟล์อัปโหลดต้นฉบับ |
|
||||
| `storedName` | varchar | ชื่อไฟล์บนดิสก์ |
|
||||
| `mimeType` | varchar | ชนิด MIME |
|
||||
| `size` | integer | ขนาดไฟล์เป็นไบต์ |
|
||||
| `width` | integer | ความกว้างของภาพเป็นพิกเซล |
|
||||
| `height` | integer | ความสูงของภาพเป็นพิกเซล |
|
||||
| `version` | integer | หมายเลขเวอร์ชัน (1 = ต้นฉบับ) |
|
||||
| `parentId` | uuid หรือ null | FK ไปยัง user_files (เวอร์ชันแม่) |
|
||||
| `toolChain` | jsonb | รหัสเครื่องมือที่นำมาใช้ตามลำดับเพื่อสร้างเวอร์ชันนี้ |
|
||||
| `createdAt` | timestamp | เวลาที่สร้าง |
|
||||
|
||||
### jobs {#jobs}
|
||||
|
||||
ติดตามงานประมวลผลสำหรับการรายงานความคืบหน้าและการล้างข้อมูล
|
||||
|
||||
| คอลัมน์ | ชนิด | หมายเหตุ |
|
||||
|---|---|---|
|
||||
| `id` | uuid | คีย์หลัก |
|
||||
| `type` | varchar | ตัวระบุเครื่องมือหรือ pipeline |
|
||||
| `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 | FK ไปยัง users |
|
||||
| `action` | varchar | ชนิดการกระทำ |
|
||||
| `details` | jsonb | ข้อมูลเฉพาะการกระทำ |
|
||||
| `createdAt` | timestamp | เวลาที่กระทำ |
|
||||
|
||||
## การย้ายข้อมูล (Migrations) {#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}
|
||||
|
||||
ฐานข้อมูลเชิงสัมพันธ์อยู่ใน volume `SnapOtter-pgdata` ของคอนเทนเนอร์ Postgres ไม่ใช่ volume `/data` ของแอป
|
||||
|
||||
**ตัวเลือกที่ 1: pg_dump (แนะนำ)**
|
||||
|
||||
```bash
|
||||
# Dump the database while the stack is running
|
||||
docker exec SnapOtter-postgres pg_dump -U snapotter snapotter > backup.sql
|
||||
|
||||
# Restore into a fresh database
|
||||
cat backup.sql | docker exec -i SnapOtter-postgres psql -U snapotter snapotter
|
||||
```
|
||||
|
||||
**ตัวเลือกที่ 2: Volume snapshot**
|
||||
|
||||
```bash
|
||||
# Stop the stack, then snapshot the pgdata volume
|
||||
docker compose down
|
||||
docker run --rm -v SnapOtter-pgdata:/data -v $(pwd)/backup:/backup \
|
||||
alpine tar czf /backup/snapotter-pgdata.tar.gz -C /data .
|
||||
```
|
||||
|
||||
### การย้ายข้อมูลจาก 1.x (SQLite) {#migrating-from-1-x-sqlite}
|
||||
|
||||
การอัปเกรดจาก SnapOtter 1.x มีคู่มือของตัวเอง ดู [การอัปเกรดจาก 1.x ไปยัง 2.0](./upgrading) สรุปสั้นๆ ให้ใช้ volume `/data` ที่มีอยู่เดิมซ้ำ และ 2.0 จะตรวจจับและนำเข้า `/data/snapotter.db` โดยอัตโนมัติเมื่อบูตครั้งแรก (หรือกำหนด `SQLITE_MIGRATE_PATH` เพื่อชี้ไปยังไฟล์นั้นอย่างชัดเจน) สำรอง volume `/data` ทั้งหมดก่อน ไม่ใช่แค่ `snapotter.db`: 1.x ใช้โหมด SQLite WAL ดังนั้นคอนเทนเนอร์ที่หยุดทำงานมักจะทิ้งข้อมูลส่วนใหญ่ไว้ใน `snapotter.db-wal` ข้างๆ `snapotter.db` ที่แทบจะว่างเปล่า
|
||||
@@ -0,0 +1,571 @@
|
||||
---
|
||||
description: "ปรับใช้ SnapOtter สู่โปรดักชันด้วย Docker ความต้องการฮาร์ดแวร์ การตั้งค่า GPU และคอนฟิก reverse proxy สำหรับ Nginx, Traefik และ Cloudflare"
|
||||
i18n_source_hash: 6b6957060fa6
|
||||
i18n_provenance: machine
|
||||
i18n_output_hash: be511d787800
|
||||
---
|
||||
|
||||
# Deployment {#deployment}
|
||||
|
||||
SnapOtter ปรับใช้เป็นสแตก Docker Compose แบบ 3 คอนเทนเนอร์: อิมเมจแอป SnapOtter, PostgreSQL 17 และ Redis 8 อิมเมจแอปรองรับ **linux/amd64** (พร้อม NVIDIA CUDA สำหรับการเร่งความเร็ว AI) และ **linux/arm64** (CPU) จึงทำงานได้แบบเนทีฟบนเซิร์ฟเวอร์ Intel/AMD, Mac ที่ใช้ Apple Silicon และอุปกรณ์ ARM อย่าง Raspberry Pi 4/5 ปัจจุบันยังไม่รองรับการเร่งความเร็วด้วย iGPU ของ Intel/AMD ผ่าน VA-API, Quick Sync หรือ OpenCL สำหรับการอนุมาน AI
|
||||
|
||||
ดู [Docker Image](./docker-tags) สำหรับการตั้งค่า GPU ตัวอย่าง Docker Compose และการปักหมุดเวอร์ชัน
|
||||
|
||||
## Quick Start (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 แทน ทั้งสอง registry จะได้รับอิมเมจเดียวกันในทุกรีลีส
|
||||
|
||||
## Quick Start (NVIDIA CUDA) {#quick-start-nvidia-cuda}
|
||||
|
||||
สำหรับการเร่งความเร็วด้วย NVIDIA CUDA บนเครื่องมือ AI (การลบพื้นหลัง, การขยายภาพ, การปรับปรุงใบหน้า, OCR):
|
||||
|
||||
```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 {#hardware-requirements}
|
||||
|
||||
ตัวเลขเหล่านี้มาจากการทดสอบประสิทธิภาพบนระบบหลากหลาย ตั้งแต่เวิร์กสเตชัน amd64 รุ่นใหม่ที่มี NVIDIA RTX 4070 ไปจนถึง Raspberry Pi โดยรันแคตตาล็อกเครื่องมือทั้งชุดบนแต่ละเครื่อง และกวาดค่าขีดจำกัดทรัพยากรของ Docker เพื่อหาขีดต่ำสุดที่แท้จริง
|
||||
|
||||
### Quick Reference {#quick-reference}
|
||||
|
||||
| ระดับ | กรณีใช้งาน | CPU | RAM | GPU | พื้นที่เก็บข้อมูล |
|
||||
|------|----------|-----|-----|-----|---------|
|
||||
| ขั้นต่ำ | เครื่องมือรูปภาพ ไฟล์ และ PDF แบบเบา; ผู้ใช้คนเดียว; ชุดงานเล็ก | 2 คอร์ | 2 GB | ไม่มี | ~7 GB |
|
||||
| แนะนำ | ครบทั้งห้าโมดัลลิตี รวมถึงวิดีโอ, PDF และ AI บน CPU; ชุดงาน; ผู้ใช้ไม่กี่คน | 4 คอร์ | 4 GB | ไม่มี | ~25 GB |
|
||||
| เต็มรูปแบบ | ทุกอย่างแบบเร็ว รวมถึง GPU AI; ชุดงานขนาดใหญ่; ผู้ใช้จำนวนมาก | 6-8 คอร์ | 8 GB | NVIDIA VRAM 8 GB ขึ้นไป (12 GB จะสบายกว่า) | ~35 GB |
|
||||
|
||||
**สถาปัตยกรรม: 64 บิตเท่านั้น** (`linux/amd64` หรือ `linux/arm64`) SnapOtter ทำงานแบบเนทีฟบนเซิร์ฟเวอร์ Intel/AMD, Mac ที่ใช้ Apple Silicon และบอร์ด ARM แบบ 64 บิต รวมถึง **Raspberry Pi 4 และ 5** (4-8 GB) มัน **ไม่** ทำงานบน ARM แบบ 32 บิต (`armv7`/`armhf`) เพราะไม่มีการสร้างอิมเมจสำหรับสถาปัตยกรรมนั้น และไม่ทำงานบนบอร์ดระดับ 512 MB อย่าง Pi Zero ซึ่งอยู่ต่ำกว่าขีดต่ำสุดของหน่วยความจำ (ดูด้านล่าง)
|
||||
|
||||
### Minimum (เครื่องมือรูปภาพ ไฟล์ และ PDF แบบเบา; ไม่มี AI) {#minimum-image-files-and-light-pdf-tools-no-ai}
|
||||
|
||||
| ทรัพยากร | ความต้องการ |
|
||||
|---|---|
|
||||
| CPU | 2 คอร์ |
|
||||
| RAM | 2 GB |
|
||||
| ดิสก์ | ~5.5 GB (อิมเมจ) + วอลุ่มข้อมูล |
|
||||
| GPU | ไม่จำเป็น |
|
||||
|
||||
เครื่องมือในแคตตาล็อกที่ไม่ใช่ AI ทั้ง 222 รายการ ได้แก่ รูปภาพ (ปรับขนาด, ครอป, แปลง, บีบอัด, ปรับแต่ง, ลายน้ำ), วิดีโอ (ตัด, ปิดเสียง, remux), เสียง (แปลง, นอร์มัลไลซ์, ตัด), 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 หนักคือการเข้ารหัสวิดีโอใหม่** การดำเนินการแบบ stream-copy (ตัด, ปิดเสียง, remux คอนเทนเนอร์) เกิดขึ้นทันที แต่การทรานส์โค้ดไปยัง codec อื่นถูกจำกัดด้วย CPU คลิป 1080p ยาว 45 วินาทีที่เข้ารหัสใหม่เป็น VP9 (WebM) ใช้เวลาราว **~40 วินาที** บน CPU รุ่นใหม่ที่เร็ว, ~45 วินาทีบน Apple Silicon, ~80 วินาทีบน mobile 4 คอร์รุ่นเก่า และ **~130 วินาที** บนเซิร์ฟเวอร์ 4 คอร์รุ่นเก่า หากงานของคุณเน้นวิดีโอ ให้เน้นจำนวนคอร์ CPU และความเร็วสัญญาณนาฬิกา หรือเพิ่มขีดจำกัด `cpus:` ของคอนเทนเนอร์ โดยค่าเริ่มต้น compose ที่ให้มาจำกัดแอปไว้ที่ 4 คอร์ (8 คอร์บน compose ของ GPU)
|
||||
|
||||
### Recommended (เครื่องมือ AI บน CPU) {#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 จริงๆ วัดผลบน CPU 4 คอร์รุ่นใหม่:
|
||||
|
||||
| เครื่องมือ AI | เวลาบน CPU | ใช้งานบน CPU ได้ไหม? |
|
||||
|---|---|---|
|
||||
| การตรวจจับใบหน้า (เบลอใบหน้า, ครอปอัจฉริยะ, ตาแดง), การลบสัญญาณรบกวน | ต่ำกว่า 1 วินาที | ได้ |
|
||||
| OCR, การถอดเสียง, คำบรรยาย | 1-3 วินาที | ได้ |
|
||||
| ลงสี, ปรับปรุงใบหน้า | ~10 วินาที | ได้ |
|
||||
| การลบ / แทนที่ / เบลอพื้นหลัง | ~29 วินาที | ได้ (ต้องรอ) |
|
||||
| การขยายภาพ AI (RealESRGAN) | ~33 วินาทีสำหรับภาพเล็ก; หลายนาทีสำหรับภาพใหญ่ | ก้ำกึ่ง แนะนำให้ใช้ GPU อย่างยิ่ง |
|
||||
| การฟื้นฟูภาพถ่าย (ไปป์ไลน์เต็มรูปแบบ) | หลายนาที | ไม่ได้ ต้องการ GPU หรือ CPU หลายคอร์ที่เร็ว |
|
||||
|
||||
SnapOtter จงใจไม่อบการดาวน์โหลดโมเดลเหล่านี้ลงในอิมเมจ Docker บันเดิล AI จะถูกดึงเมื่อผู้ดูแลระบบเปิดใช้เครื่องมือที่เกี่ยวข้องเท่านั้น เก็บไว้ในวอลุ่มถาวร `/data/ai` และแชร์ร่วมกันโดยทุกเครื่องมือที่พึ่งพาชุดโมเดลเดียวกัน วิธีนี้ทำให้อิมเมจคอนเทนเนอร์สุดท้ายมีขนาดเล็ก ในขณะที่ยังปล่อยให้การติดตั้ง AI เต็มรูปแบบไปถึงตัวเลขพื้นที่เก็บข้อมูลที่ใหญ่ขึ้นด้านล่าง
|
||||
|
||||
บางเครื่องมือพึ่งพาบันเดิลที่แชร์กันมากกว่าหนึ่งชุด ตัวอย่างเช่น Passport Photo ต้องการทั้ง `background-removal` และ `face-detection`; หากติดตั้ง `background-removal` ไว้แล้ว การเปิดใช้ Passport Photo จะดาวน์โหลดเฉพาะบันเดิล `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
|
||||
```
|
||||
|
||||
### Full (เครื่องมือ AI บน NVIDIA CUDA) {#full-ai-tools-on-nvidia-cuda}
|
||||
|
||||
| ทรัพยากร | ความต้องการ |
|
||||
|---|---|
|
||||
| CPU | 6-8 คอร์ (การเตรียมวิดีโอ + การทำงานพร้อมกันรันบน CPU แม้จะใช้ GPU AI) |
|
||||
| RAM | 8 GB |
|
||||
| GPU | NVIDIA ที่มี VRAM 8 GB ขึ้นไป (แนะนำ 12 GB) |
|
||||
| ดิสก์ | ~35 GB รวม |
|
||||
|
||||
GPU ของ NVIDIA (CUDA) เร่งความเร็วโมเดล AI ที่หนักได้อย่างมาก วัดผลบน RTX 4070 เทียบกับ CPU รุ่นใหม่:
|
||||
|
||||
| เครื่องมือ AI | ความเร็วที่เพิ่มขึ้นด้วย GPU | หมายเหตุ |
|
||||
|---|---|---|
|
||||
| การขยายภาพ AI (RealESRGAN 2×) | **~47×** | ชัยชนะที่ใหญ่ที่สุด ต่ำกว่าหนึ่งวินาที เทียบกับ ~33 วินาที (หลายนาทีสำหรับภาพใหญ่) |
|
||||
| การปรับปรุงใบหน้า (CodeFormer) | **~12×** | ~0.9 วินาที เทียบกับ ~11 วินาที |
|
||||
| การถอดเสียง (Whisper) | ~4.5× | |
|
||||
| การลบ / แทนที่ / เบลอพื้นหลัง | ~4× | ~7 วินาทีบน GPU เทียบกับ ~29 วินาทีบน CPU |
|
||||
| ลงสี | ~1.8× | |
|
||||
| OCR, การตรวจจับใบหน้า, ตาแดง, การลบสัญญาณรบกวน | ~1× | เร็วอยู่แล้วบน CPU GPU ไม่ช่วย |
|
||||
| การฟื้นฟูภาพถ่าย | ไม่มี | ถูกจำกัดด้วย CPU แม้บน GPU (ใช้ GPU 0%); CPU ที่เร็วสำคัญกว่า GPU ในกรณีนี้ |
|
||||
|
||||
เครื่องมือที่คุ้มค่ากับ GPU คือ **การขยายภาพ, การปรับปรุงใบหน้า, การถอดเสียง และการลบพื้นหลัง** การตรวจจับใบหน้า, OCR และตาแดงถูกจำกัดด้วย CPU และเร็วอยู่แล้ว ดังนั้น GPU จึงไม่เพิ่มอะไร
|
||||
|
||||
การใช้ VRAM สูงสุดพุ่งถึง 7.5 GB ระหว่างการขยายภาพพร้อมการปรับปรุงใบหน้า GPU ของ NVIDIA ขนาด 6 GB ใช้ได้กับเครื่องมือ AI ส่วนใหญ่ทีละตัว แต่จะล้มเหลวกับการขยายภาพ VRAM 8-12 GB จัดการได้ทุกอย่าง
|
||||
|
||||
ปัจจุบันยังไม่รองรับการเร่งความเร็วด้วย iGPU ของ Intel/AMD ผ่าน VA-API, Quick Sync หรือ OpenCL สำหรับการอนุมาน AI การแมป `/dev/dri` เข้าไปในคอนเทนเนอร์ไม่ได้เปิดใช้การเร่งความเร็ว AI ด้วย GPU; SnapOtter จะรันเครื่องมือ AI บน CPU เว้นแต่จะมี NVIDIA CUDA
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '4'
|
||||
memory: 8G
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
```
|
||||
|
||||
### Concurrent Users {#concurrent-users}
|
||||
|
||||
คำขอปรับขนาดรูปภาพแบบขนานที่ยิงไปยังคอนเทนเนอร์แอปซึ่งจำกัดไว้ที่ 4 คอร์โดยค่าเริ่มต้น:
|
||||
|
||||
| คำขอพร้อมกัน | เวลาตอบสนองเฉลี่ย | ข้อผิดพลาด |
|
||||
|---|---|---|
|
||||
| 1 | 0.4 วินาที | 0 |
|
||||
| 5 | 1.2 วินาที | 0 |
|
||||
| 10 | 2.1 วินาที | 0 |
|
||||
|
||||
เวลาตอบสนองลดลงแบบต่ำกว่าเชิงเส้นโดยไม่มีข้อผิดพลาดเมื่อ worker pool เต็ม การเพิ่มขีดจำกัด `cpus:` ของคอนเทนเนอร์แอป (หรือใช้โฮสต์ที่มีคอร์มากกว่า) จะยกเพดานขึ้น โปรดทราบว่างานหนัก (การทรานส์โค้ดวิดีโอ, CPU AI) จะยึด worker ไว้ตลอดระยะเวลาทั้งหมด ดังนั้นให้กำหนดขนาด CPU ตามจำนวนงานหนักที่คาดว่าจะทำพร้อมกัน ไม่ใช่แค่จำนวนคำขอ
|
||||
|
||||
### Supported Image Formats {#supported-image-formats}
|
||||
|
||||
SnapOtter รองรับ **รูปแบบอินพุต 55+ รูปแบบ** และ **รูปแบบเอาต์พุต 14 รูปแบบ** รวมถึงไฟล์ RAW จากกล้อง 20+ แบรนด์ รูปแบบระดับมืออาชีพ (PSD, EPS, OpenEXR, HDR), codec สมัยใหม่ (JPEG XL, AVIF, HEIC, QOI) และรูปแบบทางวิทยาศาสตร์/เกม (FITS, DDS)
|
||||
|
||||
ดู [รายการรูปแบบทั้งหมด](/th/guide/supported-formats) สำหรับรายละเอียดของทุกรูปแบบที่รองรับ ตัวถอดรหัสที่ใช้ และตัวควบคุมคุณภาพที่มีให้
|
||||
|
||||
### Known Limitations {#known-limitations}
|
||||
|
||||
- **Content-aware resize** ล้มเหลวกับภาพขนาดใหญ่ (>5 MP) เนื่องจากข้อจำกัดในไบนารี caire ทำงานได้ดีกับภาพขนาดเล็กกว่า
|
||||
- **การถอดรหัส HEIF** ใช้เวลา 13-23 วินาที HEIC (รุ่นของ Apple) เร็วกว่ามากที่ 0.3-0.9 วินาที
|
||||
- **OCR ภาษาญี่ปุ่น** ล้มเหลวบน CPU เนื่องจากบั๊ก MKLDNN ของ PaddlePaddle ทำงานได้บน GPU
|
||||
- **การขยายภาพ** หมดเวลาบน CPU สำหรับทุกอย่างที่เกินภาพขนาดเล็ก ต้องใช้ GPU สำหรับการใช้งานจริง
|
||||
- **CodeFormer** ปรับปรุงใบหน้าช้ากว่า GFPGAN อย่างมีนัยสำคัญ (53 วินาที เทียบกับ 2 วินาทีบน GPU) แนะนำ GFPGAN สำหรับกรณีใช้งานส่วนใหญ่
|
||||
|
||||
## Volumes {#volumes}
|
||||
|
||||
| เมานต์ / วอลุ่ม | วัตถุประสงค์ | จำเป็นไหม? |
|
||||
|---|---|---|
|
||||
| `/data` (แอป) | โมเดล AI, Python venv, ไฟล์ผู้ใช้ | **ใช่** ไฟล์จะสูญหายหากไม่มี |
|
||||
| `/tmp/workspace` (แอป) | ไฟล์ประมวลผลชั่วคราว (ทำความสะอาดอัตโนมัติ) | แนะนำ |
|
||||
| `SnapOtter-pgdata` (postgres) | ไดเรกทอรีข้อมูล PostgreSQL (ผู้ใช้, การตั้งค่า, ไปป์ไลน์, งาน) | **ใช่** ข้อมูลจะสูญหายหากไม่มี |
|
||||
| `SnapOtter-redisdata` (redis) | ไฟล์ append-only ของ Redis สำหรับคิวงานแบบทนทาน | แนะนำ |
|
||||
|
||||
### Bind mounts vs. named volumes {#bind-mounts-vs-named-volumes}
|
||||
|
||||
**Named volumes** (แนะนำ) Docker จัดการสิทธิ์ให้โดยอัตโนมัติ:
|
||||
```yaml
|
||||
volumes:
|
||||
- SnapOtter-data:/data
|
||||
```
|
||||
|
||||
**Bind mounts** คุณจัดการสิทธิ์เอง ตั้งค่า `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 {#storage-permissions}
|
||||
|
||||
SnapOtter เขียนลงสองตำแหน่งขณะรันไทม์: `/data` (ไฟล์ผู้ใช้, ล็อก, โมเดล AI และ Python venv) และ `/tmp/workspace` (พื้นที่ประมวลผลชั่วคราว) ทั้งสองตำแหน่งต้องเขียนได้โดยผู้ใช้ที่คอนเทนเนอร์รันอยู่ หากตำแหน่งใดเขียนไม่ได้ คอนเทนเนอร์จะ **ล้มเหลวทันทีตอนเริ่มทำงาน** พร้อมข้อความที่ระบุชื่อไดเรกทอรี, UID/GID ที่กำลังรัน และวิธีแก้ไข แทนที่จะบูตแบบ \"สุขภาพดี\" แล้วล้มเหลวตอนอัปโหลดครั้งแรกด้วยข้อผิดพลาดที่เข้าใจยาก
|
||||
|
||||
วิธีจัดการสิทธิ์ขึ้นอยู่กับวิธีที่คอนเทนเนอร์ถูกเปิดใช้:
|
||||
|
||||
**ค่าเริ่มต้น (เริ่มเป็น root แล้วลดสิทธิ์เป็น `snapotter`)** entrypoint เริ่มเป็น root แก้ไขความเป็นเจ้าของของวอลุ่มที่เมานต์ไว้ จากนั้นลดสิทธิ์เป็นผู้ใช้ `snapotter` ที่ไม่มีสิทธิ์พิเศษผ่าน `gosu` Named volumes ทำงานได้โดยไม่ต้องตั้งค่าใด สำหรับ bind mounts ให้ตั้งค่า `PUID`/`PGID` เป็นผู้ใช้บนโฮสต์ของคุณ (ด้านบน) เพื่อให้ไฟล์ที่มันเขียนเป็นของคุณ
|
||||
|
||||
**Kubernetes / OpenShift (non-root ผ่าน `runAsUser`)** เมื่อเปิดใช้เป็นผู้ใช้ที่ไม่ใช่ root โดยตรง คอนเทนเนอร์ไม่สามารถ chown วอลุ่มได้เอง ดังนั้น orchestrator ต้องทำให้มันเขียนได้ ตั้งค่า `fsGroup`:
|
||||
|
||||
```yaml
|
||||
securityContext:
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
fsGroup: 999 # makes mounted volumes writable by the pod
|
||||
```
|
||||
|
||||
ไดเรกทอรีที่เขียนได้ของอิมเมจเป็นของกลุ่ม GID 0 และกลุ่มสามารถเขียนได้ ดังนั้น pod ที่รันด้วย **UID ใดก็ได้** พร้อมกลุ่มเสริม root (ค่าเริ่มต้นของ OpenShift) สามารถเขียนได้โดยไม่ต้อง `chown`
|
||||
|
||||
**TrueNAS Scale (และการตั้งค่า \"UID ต่างถิ่น\" อื่นๆ)** TrueNAS รันแอปเป็นผู้ใช้ที่ไม่ใช่ root (มักเป็น `568:568`) และเมานต์ dataset ของโฮสต์ที่เป็นของผู้ใช้อื่น ดังนั้นทั้ง entrypoint และ `fsGroup` ต่างก็ไม่ทำให้มันเขียนได้ด้วยตัวเอง เลือกอย่างใดอย่างหนึ่ง:
|
||||
|
||||
- **รันแอปเป็น root** (แนะนำ) ปล่อยให้ผู้ใช้ของแอปไม่ได้ตั้งค่า หรือตั้งเป็น `0` แล้วให้ entrypoint ค่าเริ่มต้นแก้ไขสิทธิ์และลดสิทธิ์เป็น `snapotter`
|
||||
- **รันเป็น UID `999`** ตั้งค่าผู้ใช้/กลุ่มของแอปเป็น `999:999` (ผู้ใช้ `snapotter` ในตัวของ SnapOtter) เพื่อให้ตรงกับความเป็นเจ้าของของอิมเมจ
|
||||
- **`chown` dataset ของโฮสต์** เป็น UID ที่คอนเทนเนอร์รันอยู่ จากเชลล์ของ TrueNAS:
|
||||
|
||||
```bash
|
||||
# ใช้ UID จากข้อผิดพลาดตอนเริ่มทำงาน (หรือรัน `id` ในคอนเทนเนอร์)
|
||||
chown -R 568:568 /mnt/<pool>/<dataset>
|
||||
```
|
||||
|
||||
ข้อผิดพลาดตอนเริ่มทำงานจะระบุ UID ที่แน่นอนให้ใช้ ดังนั้นเส้นทางที่เร็วที่สุดคือเริ่มแอปหนึ่งครั้ง อ่านข้อความ แล้ว `chown` (หรือปรับผู้ใช้) ตามนั้น
|
||||
|
||||
## Environment Variables {#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` | คำขอ API ต่อนาทีต่อ IP (ตั้ง 0 เพื่อปิด) |
|
||||
| `MAX_USERS` | `0` (ไม่จำกัด) | จำนวนบัญชีผู้ใช้สูงสุด |
|
||||
| `TRUST_PROXY` | `true` | เชื่อถือส่วนหัว X-Forwarded-For จาก reverse proxy |
|
||||
| `PUID` | `999` | รันเป็น UID นี้ (สำหรับสิทธิ์ bind mount) |
|
||||
| `PGID` | `999` | รันเป็น GID นี้ (สำหรับสิทธิ์ bind mount) |
|
||||
| `LOG_LEVEL` | `info` | ระดับความละเอียดของล็อก: fatal, error, warn, info, debug, trace |
|
||||
| `CONCURRENT_JOBS` | `0` (อัตโนมัติ) | จำนวนงานประมวลผล AI แบบขนานสูงสุด |
|
||||
| `SESSION_DURATION_HOURS` | `168` | อายุของเซสชันการล็อกอิน (7 วัน) |
|
||||
| `CORS_ORIGIN` | (ว่าง) | origin ที่อนุญาตคั่นด้วยคอมมา หรือปล่อยว่างสำหรับ same-origin |
|
||||
|
||||
## Health Check {#health-check}
|
||||
|
||||
คอนเทนเนอร์มี 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 {#reverse-proxy}
|
||||
|
||||
SnapOtter ตั้งค่า `TRUST_PROXY=true` โดยค่าเริ่มต้น เพื่อให้การจำกัดอัตราและการบันทึกล็อกใช้ IP จริงของไคลเอนต์จากส่วนหัว `X-Forwarded-For`
|
||||
|
||||
### 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** รันอัตโนมัติในทุก push และ PR ทำ lint, typecheck, ทดสอบ, build และตรวจสอบอิมเมจ Docker (โดยไม่ push)
|
||||
- **release.yml** ทริกเกอร์ด้วยตนเองผ่าน `workflow_dispatch` รัน semantic-release เพื่อสร้าง version tag และ GitHub release จากนั้นสร้างอิมเมจ Docker แบบหลายสถาปัตยกรรม (amd64 + arm64) และ push ไปยัง Docker Hub (`snapotter/snapotter`) และ GitHub Container Registry (`ghcr.io/snapotter-hq/snapotter`)
|
||||
- **deploy-docs.yml** สร้างไซต์เอกสารนี้และปรับใช้ไปยัง Cloudflare Pages เมื่อ push ไปยัง `main`
|
||||
|
||||
หากต้องการสร้างรีลีส ให้ไปที่ **Actions > Release > Run workflow** ใน GitHub UI หรือรัน:
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml
|
||||
```
|
||||
|
||||
Semantic-release กำหนดเวอร์ชันจากประวัติคอมมิต แท็ก Docker `latest` ชี้ไปยังรีลีสล่าสุดเสมอ
|
||||
|
||||
## Analytics {#analytics}
|
||||
|
||||
SnapOtter มีการวิเคราะห์ผลิตภัณฑ์แบบไม่ระบุตัวตน (รูปแบบการใช้เครื่องมือ, รายงานข้อผิดพลาด) เพื่อช่วยจับบั๊กและปรับปรุงฟีเจอร์ โดยเปิดใช้เป็นค่าเริ่มต้น ไฟล์ ชื่อไฟล์ และข้อมูลส่วนบุคคลของคุณไม่เคยเป็นส่วนหนึ่งของสิ่งนี้ SnapOtter ทำงานได้ตามปกติเมื่อปิดการวิเคราะห์
|
||||
|
||||
### Disabling analytics {#disabling-analytics}
|
||||
|
||||
การเลือกไม่เข้าร่วมขณะรันไทม์เป็นสวิตช์แอดมินแบบคลิกเดียว เปิด Settings > System > Privacy แล้วปิด Anonymous Product Analytics มันจะหยุดทันทีสำหรับทั้งอินสแตนซ์ โดยไม่ต้อง build ใหม่
|
||||
|
||||
สำหรับอิมเมจที่ไม่มีทางส่งการวิเคราะห์ได้เลย ให้ตั้งค่าการปิดแบบถาวรตอน build โดยโคลนที่เก็บและ build ใหม่:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
หรือเพิ่ม build arg ลงใน `docker-compose.yml` ที่มีอยู่ของคุณ:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
snapotter:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
args:
|
||||
SNAPOTTER_ANALYTICS: "off"
|
||||
```
|
||||
@@ -0,0 +1,234 @@
|
||||
---
|
||||
description: "การตั้งค่าสภาพแวดล้อมการพัฒนาในเครื่อง คำสั่ง แนวทางการเขียนโค้ด และวิธีเพิ่มเครื่องมือใหม่ใน SnapOtter"
|
||||
i18n_source_hash: cb03724d2829
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 66d6a87e1e3d
|
||||
---
|
||||
|
||||
# คู่มือนักพัฒนา {#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 ในเครื่อง, การ build คอนเทนเนอร์ และฟีเจอร์ AI)
|
||||
- Git
|
||||
|
||||
ต้องมี Python 3.10+ เฉพาะเมื่อคุณกำลังทำงานกับ AI/ML sidecar (การลบพื้นหลัง, การขยายภาพ, OCR) เท่านั้น
|
||||
|
||||
## การตั้งค่า {#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
|
||||
```
|
||||
|
||||
สิ่งนี้เริ่มต้น dev server สองตัว:
|
||||
|
||||
| บริการ | URL | หมายเหตุ |
|
||||
|----------|--------------------------|------------------------------------|
|
||||
| Frontend | http://localhost:1349 | Vite dev server, proxy /api |
|
||||
| Backend | http://localhost:13490 | Fastify API (เข้าถึงผ่าน proxy) |
|
||||
|
||||
เปิด 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}
|
||||
|
||||
- Double quotes, semicolons, การเยื้อง 2 ช่องว่าง (บังคับใช้โดย Biome)
|
||||
- ES modules ในทุก workspace
|
||||
- [Conventional commits](https://www.conventionalcommits.org/) สำหรับ semantic-release
|
||||
- Zod สำหรับการตรวจสอบอินพุต API ทั้งหมด
|
||||
- ไม่แก้ไขไฟล์การตั้งค่าของ Biome, TypeScript หรือ editor แก้ไขโค้ด ไม่ใช่ linter
|
||||
|
||||
## ฐานข้อมูล {#database}
|
||||
|
||||
PostgreSQL 17 ผ่าน Drizzle ORM (pg-core) การพัฒนาในเครื่องต้องมี Postgres และ Redis ทำงานอยู่ - เริ่มต้นด้วย:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
สิ่งนี้ให้ Postgres บนพอร์ต 5432 และ Redis บนพอร์ต 6379 จากนั้นสร้างและนำการย้ายข้อมูลมาใช้:
|
||||
|
||||
```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. Backend route {#_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. คอมโพเนนต์การตั้งค่า Frontend {#_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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
จากนั้นลงทะเบียนใน frontend tool registry ที่ `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 สามารถกำหนดเป้าหมายได้อย่างน่าเชื่อถือ
|
||||
|
||||
## การ build Docker {#docker-builds}
|
||||
|
||||
Build อิมเมจโปรดักชันเต็มรูปแบบในเครื่อง:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t snapotter:latest .
|
||||
```
|
||||
|
||||
ใช้ BuildKit cache mount เพื่อการ rebuild ที่เร็วขึ้น:
|
||||
|
||||
```bash
|
||||
DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile -t snapotter:latest .
|
||||
```
|
||||
|
||||
## ตัวแปรสภาพแวดล้อม {#environment-variables}
|
||||
|
||||
ดู [คู่มือการกำหนดค่า](/th/guide/configuration) สำหรับรายการทั้งหมด ตัวแปรสำคัญสำหรับการพัฒนา:
|
||||
|
||||
| ตัวแปร | ค่าเริ่มต้น | คำอธิบาย |
|
||||
|-----------------------------|-----------|------------------------------------------------|
|
||||
| `AUTH_ENABLED` | `true` | เปิด/ปิดการยืนยันตัวตน |
|
||||
| `DEFAULT_USERNAME` | `admin` | ชื่อผู้ใช้ admin เริ่มต้น |
|
||||
| `DEFAULT_PASSWORD` | `admin` | รหัสผ่าน admin เริ่มต้น |
|
||||
| `SKIP_MUST_CHANGE_PASSWORD` | `false` | ข้ามการบังคับเปลี่ยนรหัสผ่าน (CI/dev เท่านั้น) |
|
||||
| `RATE_LIMIT_PER_MIN` | `1000` | ขีดจำกัดอัตรา API ต่อนาที (0 = ปิด) |
|
||||
| `MAX_UPLOAD_SIZE_MB` | `100` | ขนาดอัปโหลดสูงสุดเป็น MB (0 = ไม่จำกัด) |
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
description: "แท็กของ Docker image สำหรับ SnapOtter, การเปรียบเทียบประสิทธิภาพ GPU, การล็อกเวอร์ชัน และการรองรับหลายแพลตฟอร์มสำหรับ AMD64 และ ARM64"
|
||||
i18n_source_hash: 148b3608e11a
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 6df1f68453ec
|
||||
---
|
||||
|
||||
# Docker Image {#docker-image}
|
||||
|
||||
SnapOtter เผยแพร่เป็น Docker image เพียงตัวเดียว รันมันเดี่ยว ๆ แล้วมันจะเริ่ม PostgreSQL 17 และ Redis แบบฝังตัวบนอินเทอร์เฟซ loopback (โหมดฝังตัว) สำหรับการใช้งานจริง ให้รันควบคู่ไปกับคอนเทนเนอร์ PostgreSQL 17 และ Redis 8 แยกต่างหากด้วย Compose แอป image นี้ทำงานได้บนทุกแพลตฟอร์ม
|
||||
|
||||
## เริ่มต้นอย่างรวดเร็ว {#quick-start}
|
||||
|
||||
```bash
|
||||
docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data snapotter/snapotter:latest
|
||||
```
|
||||
|
||||
เมื่อไม่ได้ตั้งค่า `DATABASE_URL` ตัวนี้จะรันในโหมดฝังตัว: PostgreSQL และ Redis จะเริ่มทำงานภายในคอนเทนเนอร์บน loopback โดยเก็บข้อมูลทั้งหมดไว้ใต้ volume `SnapOtter-data` ตั้งค่า `DATABASE_URL` และ `REDIS_URL` (อย่างที่สแตก [Compose](#docker-compose) ทำ) เพื่อใช้บริการภายนอกแทน ดู [การกำหนดค่า](/th/guide/configuration#embedded-mode)
|
||||
|
||||
## การเร่งความเร็วด้วย NVIDIA CUDA {#nvidia-cuda-acceleration}
|
||||
|
||||
image นี้มีการรองรับ NVIDIA CUDA บน amd64 หากคุณมี 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
|
||||
```
|
||||
|
||||
image จะตรวจจับ CUDA โดยอัตโนมัติในขณะรัน หากไม่มี `--gpus all` หรือเมื่อ CUDA ไม่พร้อมใช้งาน เครื่องมือ AI จะรันบน CPU ใช้ image เดียวกันได้ทั้งสองแบบ
|
||||
|
||||
การเร่งความเร็วด้วย iGPU ของ Intel/AMD ผ่าน VA-API, Quick Sync หรือ OpenCL ยังไม่รองรับสำหรับการอนุมาน AI ของ SnapOtter ในปัจจุบัน การแมป `/dev/dri` เข้าไปในคอนเทนเนอร์อาจเปิดเผยอุปกรณ์ render ได้ แต่รันไทม์ AI จะยังคงใช้ CPU เว้นแต่จะมี CUDA พร้อมใช้งาน
|
||||
|
||||
### การเปรียบเทียบประสิทธิภาพ {#benchmarks}
|
||||
|
||||
ทดสอบบน NVIDIA RTX 4070 (VRAM 12 GB) ด้วยภาพบุคคล JPEG ขนาด 572x1024
|
||||
|
||||
#### ประสิทธิภาพแบบ warm {#warm-performance}
|
||||
|
||||
| เครื่องมือ | CPU | GPU | เร็วขึ้น |
|
||||
|------|-----|-----|---------|
|
||||
| การลบพื้นหลัง (u2net) | 2,415ms | 879ms | 2.7x |
|
||||
| การลบพื้นหลัง (isnet) | 2,457ms | 1,137ms | 2.2x |
|
||||
| ขยายภาพ 2x | 350ms | 309ms | 1.1x |
|
||||
| ขยายภาพ 4x | 910ms | 310ms | 2.9x |
|
||||
| OCR (PaddleOCR) | 137ms | 94ms | 1.5x |
|
||||
| เบลอใบหน้า | 139ms | 122ms | 1.1x |
|
||||
|
||||
#### Cold start (คำขอแรกหลังเริ่มคอนเทนเนอร์) {#cold-start-first-request-after-container-start}
|
||||
|
||||
| เครื่องมือ | CPU | GPU | เร็วขึ้น |
|
||||
|------|-----|-----|---------|
|
||||
| การลบพื้นหลัง | 22,286ms | 4,792ms | 4.7x |
|
||||
| ขยายภาพ 2x | 3,957ms | 2,318ms | 1.7x |
|
||||
| OCR (PaddleOCR) | 1,469ms | 1,090ms | 1.3x |
|
||||
|
||||
### การตรวจสอบสถานะ CUDA {#cuda-health-check}
|
||||
|
||||
หลังจากคำขอ AI ครั้งแรก endpoint สำหรับตรวจสอบสถานะของผู้ดูแลระบบจะรายงานสถานะ CUDA GPU:
|
||||
|
||||
```
|
||||
GET /api/v1/admin/health
|
||||
{"ai": {"gpu": true}}
|
||||
```
|
||||
|
||||
## Docker Compose {#docker-compose}
|
||||
|
||||
สแตก Compose แบบเต็มประกอบด้วยแอป, PostgreSQL 17 และ Redis 8 ดู [การนำไปใช้งาน](/th/guide/deployment) สำหรับ `docker-compose.yml` ฉบับสมบูรณ์ ตัวอย่างขั้นต่ำ:
|
||||
|
||||
```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:
|
||||
```
|
||||
|
||||
สำหรับการเร่งความเร็วด้วย NVIDIA CUDA ผ่าน Docker Compose ให้เพิ่มส่วน deploy เข้าไปในบริการ SnapOtter:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
```
|
||||
|
||||
## การล็อกเวอร์ชัน {#version-pinning}
|
||||
|
||||
| แท็ก | คำอธิบาย |
|
||||
|-----|------------|
|
||||
| `latest` | รุ่นล่าสุด |
|
||||
| `1.11.0` | เวอร์ชันที่ระบุแน่นอน |
|
||||
| `1.11` | patch ล่าสุดใน 1.11.x |
|
||||
| `1` | minor ล่าสุดใน 1.x |
|
||||
|
||||
## แพลตฟอร์ม {#platforms}
|
||||
|
||||
| สถาปัตยกรรม | การรองรับ GPU | หมายเหตุ |
|
||||
|---|---|---|
|
||||
| linux/amd64 | NVIDIA CUDA | การเร่งความเร็ว CUDA เต็มรูปแบบสำหรับเครื่องมือ AI |
|
||||
| linux/arm64 | CPU เท่านั้น | Raspberry Pi 4/5, Apple Silicon ผ่าน Docker Desktop |
|
||||
|
||||
## การย้ายจากแท็กก่อนหน้า {#migration-from-previous-tags}
|
||||
|
||||
หากคุณเคยใช้แท็ก `:cuda` ให้เปลี่ยนไปใช้ `:latest` และคง `--gpus all` ไว้ การรองรับ GPU เหมือนเดิม เป็น image ที่รวมเป็นหนึ่งเดียว
|
||||
|
||||
ข้อมูลและการตั้งค่าของคุณจะถูกเก็บรักษาไว้ใน volume
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
description: "ติดตั้ง SnapOtter ด้วย Docker ในคำสั่งเดียว รวมถึงการตั้งค่า Docker Compose การ build จากซอร์ส และภาพรวมฟีเจอร์ทั้งหมด"
|
||||
i18n_source_hash: 4536d4558b8e
|
||||
i18n_provenance: machine
|
||||
i18n_output_hash: 5353dda97d38
|
||||
---
|
||||
|
||||
# Getting Started {#getting-started}
|
||||
|
||||
::: tip ลองก่อนติดตั้ง
|
||||
สำรวจ UI แบบเต็มที่ [demo.snapotter.com](https://demo.snapotter.com) โดยไม่ต้องสมัครหรือติดตั้ง
|
||||
:::
|
||||
|
||||
## Quick Start {#quick-start}
|
||||
|
||||
```bash
|
||||
docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data snapotter/snapotter:latest
|
||||
```
|
||||
|
||||
คอนเทนเนอร์เดียวนี้รันทุกอย่างที่จำเป็น: เมื่อไม่ได้ตั้งค่า `DATABASE_URL` มันจะเริ่ม PostgreSQL และ Redis ของตัวเองบนอินเทอร์เฟซ loopback (โหมด embedded) และเก็บข้อมูลทั้งหมดไว้ในวอลุ่ม `SnapOtter-data` นี่คือวิธีที่เร็วที่สุดในการลอง SnapOtter หรือ self-host บน homelab สำหรับโปรดักชัน ให้รันสแตก [Docker Compose](#docker-compose) ด้านล่าง ซึ่งเก็บ PostgreSQL และ Redis ไว้ในคอนเทนเนอร์ของตัวเอง โหมด embedded รันเป็น root (ค่าเริ่มต้น) และปิดโดยอัตโนมัติทันทีที่คุณตั้งค่า `DATABASE_URL`
|
||||
|
||||
คุณจะถูกขอให้เปลี่ยนรหัสผ่านตอนล็อกอินครั้งแรก
|
||||
|
||||
::: tip การวิเคราะห์ผลิตภัณฑ์แบบไม่ระบุตัวตน
|
||||
SnapOtter มีการวิเคราะห์ผลิตภัณฑ์แบบไม่ระบุตัวตนโดยค่าเริ่มต้น หากต้องการปิด ให้เปิด **Settings → System → Privacy** แล้วปิด **Anonymous Product Analytics** มันจะหยุดทันทีสำหรับทั้งอินสแตนซ์
|
||||
|
||||
คุณยังสามารถตั้งค่าตัวแปรสภาพแวดล้อม `SNAPOTTER_TELEMETRY=0` (`false` และ `off` ก็ใช้ได้) เพื่อปิด telemetry ทั้งหมดสำหรับอินสแตนซ์โดยไม่ต้อง build ใหม่
|
||||
|
||||
การตรวจสอบข้อผิดพลาดขับเคลื่อนโดย [Sentry](https://sentry.io) ซึ่งสนับสนุน SnapOtter ผ่านโปรแกรมโอเพนซอร์สของตน
|
||||
|
||||
สำหรับรายละเอียดเกี่ยวกับสิ่งที่ถูกเก็บ ดู [สิ่งที่ SnapOtter เก็บ](/th/guide/telemetry)
|
||||
:::
|
||||
|
||||
::: tip การเร่งความเร็วด้วย NVIDIA CUDA
|
||||
เพิ่ม `--gpus all` สำหรับการลบพื้นหลัง, การขยายภาพ, OCR, การปรับปรุงใบหน้า และการฟื้นฟูที่เร่งความเร็วด้วย NVIDIA CUDA:
|
||||
|
||||
```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) จะสำรองไปใช้ CPU โดยอัตโนมัติเมื่อไม่มี CUDA ปัจจุบันยังไม่รองรับการเร่งความเร็วด้วย iGPU ของ Intel/AMD ผ่าน VA-API, Quick Sync หรือ OpenCL สำหรับการอนุมาน AI ดู [Docker Tags](/th/guide/docker-tags) สำหรับผลการทดสอบประสิทธิภาพ
|
||||
:::
|
||||
|
||||
::: details มีบน GHCR ด้วย
|
||||
```bash
|
||||
docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data ghcr.io/snapotter-hq/snapotter:latest
|
||||
```
|
||||
|
||||
ทั้งสอง registry เผยแพร่อิมเมจเดียวกันในทุกรีลีส
|
||||
:::
|
||||
|
||||
## 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](/th/guide/configuration) สำหรับตัวแปรสภาพแวดล้อมทั้งหมด
|
||||
|
||||
## Build from Source {#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
|
||||
```
|
||||
|
||||
- Frontend: [http://localhost:1349](http://localhost:1349)
|
||||
- Backend: [http://localhost:13490](http://localhost:13490)
|
||||
|
||||
## What You Can Do {#what-you-can-do}
|
||||
|
||||
### File Processing (200+ Tools) {#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 {#pipelines}
|
||||
|
||||
ร้อยเครื่องมือเข้าเป็นเวิร์กโฟลว์หลายขั้นตอน แล้วนำไปใช้กับรูปภาพเดียวหรือทั้งชุด:
|
||||
|
||||
1. เปิด **Pipelines** ในแถบด้านข้าง
|
||||
2. เพิ่มขั้นตอน (เครื่องมือใดก็ได้ การตั้งค่าใดก็ได้)
|
||||
3. รันบนไฟล์เดียว หรือทั้งชุดในคราวเดียว
|
||||
4. บันทึกไปป์ไลน์ไว้ใช้ซ้ำภายหลัง
|
||||
|
||||
ไปป์ไลน์อนุญาต 20 ขั้นตอนโดยค่าเริ่มต้น ตั้งค่า `MAX_PIPELINE_STEPS=0` เพื่อทำให้ขีดจำกัดไม่จำกัด
|
||||
|
||||
### File Library {#file-library}
|
||||
|
||||
ทุกไฟล์ที่คุณประมวลผลสามารถบันทึกไปยังไลบรารี **Files** ของคุณได้ SnapOtter ติดตามประวัติเวอร์ชันทั้งหมด เพื่อให้คุณย้อนรอยทุกขั้นตอนการประมวลผลตั้งแต่การอัปโหลดต้นฉบับจนถึงเอาต์พุตสุดท้าย
|
||||
|
||||
การบันทึกเป็นการกระทำที่ชัดเจน: ผลลัพธ์ที่คุณบันทึกไปยังไลบรารีจะถูกเก็บไว้จนกว่าคุณจะลบ ในขณะที่ผลลัพธ์ที่คุณประมวลผลและปล่อยไว้โดยไม่บันทึกจะถูกล้างโดยอัตโนมัติหลังจาก 72 ชั่วโมง (กำหนดค่าได้ผ่าน `FILE_MAX_AGE_HOURS`)
|
||||
|
||||
### REST API & API Keys {#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"}'
|
||||
```
|
||||
|
||||
สร้าง API key ภายใต้ **Settings → API Keys** ดู [REST API reference](/th/api/rest) สำหรับ endpoint ทั้งหมด หรือไปที่ [http://localhost:1349/api/docs](http://localhost:1349/api/docs) สำหรับเอกสารอ้างอิงแบบโต้ตอบ
|
||||
|
||||
### Multi-User & Teams {#multi-user-teams}
|
||||
|
||||
เปิดใช้ผู้ใช้หลายคนด้วยการควบคุมการเข้าถึงตามบทบาท:
|
||||
|
||||
- **แอดมิน**: เข้าถึงเต็มรูปแบบ จัดการผู้ใช้, ทีม, การตั้งค่า, ไฟล์/ไปป์ไลน์/API key ทั้งหมด
|
||||
- **ผู้ใช้**: ใช้เครื่องมือ, จัดการไฟล์/ไปป์ไลน์/API key ของตัวเอง
|
||||
|
||||
สร้างทีมภายใต้ **Settings → Teams** เพื่อจัดกลุ่มผู้ใช้
|
||||
|
||||
ตั้งค่า `AUTH_ENABLED=true` (หรือ `false` สำหรับการใช้งานคนเดียว/ใช้เองโดยไม่ต้องล็อกอิน)
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
description: "ตั้งค่า Single Sign-On ด้วย OpenID Connect คู่มือทีละขั้นตอนสำหรับ Keycloak, Authentik, Google และผู้ให้บริการ OIDC อื่น ๆ"
|
||||
i18n_source_hash: 4296343b3cc5
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: cf303e97a8f0
|
||||
---
|
||||
|
||||
# OIDC / Single Sign-On {#oidc-single-sign-on}
|
||||
|
||||
SnapOtter รองรับ OpenID Connect (OIDC) สำหรับ single sign-on ผู้ใช้สามารถเข้าสู่ระบบด้วยผู้ให้บริการข้อมูลประจำตัวภายนอกอย่าง Keycloak, Authentik หรือ Google แทน (หรือควบคู่ไปกับ) การยืนยันตัวตนด้วยชื่อผู้ใช้/รหัสผ่านในเครื่อง
|
||||
|
||||
::: tip ดูเพิ่มเติม
|
||||
[SAML SSO](/th/guide/saml) | [SCIM Provisioning](/th/guide/scim) | [ผู้ใช้ บทบาท และสิทธิ์](/th/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"
|
||||
```
|
||||
|
||||
redirect URI สำหรับผู้ให้บริการของคุณจะเป็นแบบนี้เสมอ:
|
||||
|
||||
```
|
||||
${EXTERNAL_URL}/api/auth/oidc/callback
|
||||
```
|
||||
|
||||
ตัวอย่างเช่น หาก `EXTERNAL_URL` คือ `https://photos.example.com` ให้กำหนด redirect URI ของผู้ให้บริการเป็น `https://photos.example.com/api/auth/oidc/callback`
|
||||
|
||||
## เอกสารอ้างอิงการกำหนดค่า {#configuration-reference}
|
||||
|
||||
| ตัวแปร | ค่าเริ่มต้น | คำอธิบาย |
|
||||
|---|---|---|
|
||||
| `OIDC_ENABLED` | `false` | เปิดใช้งานการเข้าสู่ระบบ OIDC ปุ่ม "Sign in with SSO" จะปรากฏบนหน้าเข้าสู่ระบบ |
|
||||
| `OIDC_ISSUER_URL` | | URL ผู้ออก (issuer) ของผู้ให้บริการ ต้องรองรับ OIDC Discovery (`/.well-known/openid-configuration`) |
|
||||
| `OIDC_CLIENT_ID` | | OAuth client ID ที่ลงทะเบียนกับผู้ให้บริการของคุณ |
|
||||
| `OIDC_CLIENT_SECRET` | | OAuth client secret |
|
||||
| `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` | ความคลาดเคลื่อนของนาฬิกาที่ยอมรับได้เป็นวินาทีสำหรับการตรวจสอบ token |
|
||||
| `OIDC_USERNAME_CLAIM` | `preferred_username` | claim ของ ID token ที่ใช้เป็นชื่อผู้ใช้สำหรับบัญชีใหม่ |
|
||||
| `EXTERNAL_URL` | | URL สาธารณะที่เข้าถึง SnapOtter ได้ จำเป็นสำหรับ OIDC เพื่อสร้าง redirect URI ที่ถูกต้อง |
|
||||
| `COOKIE_SECRET` | สร้างอัตโนมัติ | secret สำหรับเซ็นชื่อคุกกี้เซสชัน ตั้งค่านี้อย่างชัดเจนเมื่อรันหลาย replica |
|
||||
|
||||
## คู่มือผู้ให้บริการ {#provider-guides}
|
||||
|
||||
### Keycloak {#keycloak}
|
||||
|
||||
1. สร้าง realm ใหม่ (หรือใช้ที่มีอยู่)
|
||||
2. ไปที่ **Clients** และสร้าง client ใหม่:
|
||||
- **Client ID**: `snapotter`
|
||||
- **Client authentication**: เปิด (confidential)
|
||||
- **Authentication flow**: Standard flow (Authorization Code)
|
||||
3. ใต้แท็บ **Settings** ของ client ให้ตั้งค่า **Valid redirect URIs** เป็น callback URL ของคุณ (เช่น `https://photos.example.com/api/auth/oidc/callback`)
|
||||
4. คัดลอก **Client secret** จากแท็บ **Credentials**
|
||||
5. ตั้งค่า `OIDC_ISSUER_URL` เป็น `https://keycloak.example.com/realms/your-realm`
|
||||
|
||||
### Authentik {#authentik}
|
||||
|
||||
1. ในหน้าจอผู้ดูแลระบบ ไปที่ **Applications > Providers** และสร้าง **OAuth2/OpenID Provider** ใหม่
|
||||
- **Client type**: Confidential
|
||||
- **Redirect URIs**: callback URL ของคุณ
|
||||
- **Signing key**: เลือกคีย์ที่มีอยู่หรือสร้างขึ้นใหม่
|
||||
2. สร้าง **Application** และเชื่อมโยงกับ provider
|
||||
3. คัดลอก **Client ID** และ **Client Secret** จากการตั้งค่า provider
|
||||
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**: callback 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 ชื่อผู้ใช้จะดึงมาจาก claim ที่ระบุโดย `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 ส่วนตัว ให้ mount CA bundle เข้าไปในคอนเทนเนอร์และชี้ `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}
|
||||
|
||||
### redirect URI ไม่ตรงกัน {#redirect-uri-mismatch}
|
||||
|
||||
ข้อผิดพลาดที่พบบ่อยที่สุด ตรวจสอบความแตกต่างเหล่านี้ระหว่างสิ่งที่ผู้ให้บริการคาดหวังกับสิ่งที่ SnapOtter ส่ง:
|
||||
|
||||
- `http` กับ `https` scheme ต้องตรงกันทุกประการ
|
||||
- เครื่องหมายทับท้าย ผู้ให้บริการบางรายเข้มงวดเรื่องนี้
|
||||
- หมายเลขพอร์ต ระบุพอร์ตหากไม่ใช่ค่ามาตรฐาน
|
||||
- Path ต้องเป็น `/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 ไม่ตรงกัน การตรวจสอบ token อาจล้มเหลว เพิ่มค่า `OIDC_CLOCK_TOLERANCE` (ค่าเริ่มต้นคือ 30 วินาที) วิธีแก้ที่ดีกว่าคือรัน NTP บนทั้งสองเครื่อง
|
||||
|
||||
### "OIDC provider unreachable" {#oidc-provider-unreachable}
|
||||
|
||||
SnapOtter ดึงเอกสาร discovery ของผู้ให้บริการเมื่อเริ่มต้นและระหว่างการเข้าสู่ระบบ ตรวจสอบ:
|
||||
|
||||
- การแปลง DNS จากภายในคอนเทนเนอร์ Docker (`docker exec snapotter nslookup auth.example.com`)
|
||||
- กฎไฟร์วอลล์ระหว่างคอนเทนเนอร์และผู้ให้บริการ
|
||||
- ค่า `OIDC_ISSUER_URL` ต้องเข้าถึงได้จากเซิร์ฟเวอร์ ไม่ใช่แค่จากเบราว์เซอร์ของคุณ
|
||||
|
||||
### claim ที่ขาดหายไป {#missing-claims}
|
||||
|
||||
หากชื่อผู้ใช้หรืออีเมลว่างเปล่าหลังจากเข้าสู่ระบบ ผู้ให้บริการของคุณอาจไม่ได้ส่ง claim ที่คาดหวังกลับมา ตรวจสอบ:
|
||||
|
||||
- scope ที่กำหนดค่าใน `OIDC_SCOPES` มี `profile` และ `email`
|
||||
- ผู้ให้บริการถูกกำหนดค่าให้รวม claim ที่ระบุใน `OIDC_USERNAME_CLAIM` ไว้ใน ID token
|
||||
- ผู้ให้บริการบางรายต้องการการกำหนดค่า mapper/scope อย่างชัดเจนเพื่อปล่อย claim
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
description: "ตั้งค่า SAML 2.0 Single Sign-On สำหรับ SnapOtter คู่มือทีละขั้นตอนสำหรับ Okta, Azure AD / Entra ID, Google Workspace และผู้ให้บริการข้อมูลประจำตัว SAML อื่น ๆ"
|
||||
i18n_source_hash: 33dfb8b02a22
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: c6a0d84adefa
|
||||
---
|
||||
|
||||
# SAML SSO {#saml-sso}
|
||||
|
||||
SnapOtter รองรับ SAML 2.0 สำหรับ single sign-on ผู้ใช้สามารถเข้าสู่ระบบผ่านผู้ให้บริการข้อมูลประจำตัวภายนอก (Okta, Azure AD / Entra ID, Google Workspace หรือ SAML 2.0 IdP มาตรฐานใด ๆ) แทนการยืนยันตัวตนด้วยชื่อผู้ใช้/รหัสผ่านในเครื่อง
|
||||
|
||||
::: tip ฟีเจอร์ระดับองค์กร
|
||||
SAML SSO ต้องมีไลเซนส์ **team** หรือ **enterprise** พร้อมฟีเจอร์ `saml_sso` หากตั้งค่า `SAML_ENABLED=true` โดยไม่มีไลเซนส์ที่ถูกต้อง เส้นทาง SAML จะถูกข้ามไปอย่างเงียบ ๆ และมีการบันทึกคำเตือน
|
||||
:::
|
||||
|
||||
## สิ่งที่ต้องมีก่อน {#prerequisites}
|
||||
|
||||
- instance ของ SnapOtter ที่กำลังทำงานและเข้าถึงได้ที่ URL สาธารณะ
|
||||
- `EXTERNAL_URL` ตั้งค่าเป็น URL สาธารณะนั้น (เช่น `https://photos.example.com`)
|
||||
- คีย์ไลเซนส์ team หรือ enterprise พร้อมฟีเจอร์ `saml_sso`
|
||||
- สิทธิ์ผู้ดูแลระบบสำหรับผู้ให้บริการข้อมูลประจำตัว 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
|
||||
```
|
||||
|
||||
รีสตาร์ทคอนเทนเนอร์ ปุ่ม "Sign in with SAML" (หรือป้ายที่ตั้งโดย `SAML_PROVIDER_NAME`) จะปรากฏบนหน้าเข้าสู่ระบบ
|
||||
|
||||
## เอกสารอ้างอิงการกำหนดค่า {#configuration-reference}
|
||||
|
||||
| ตัวแปร | ค่าเริ่มต้น | คำอธิบาย |
|
||||
|---|---|---|
|
||||
| `SAML_ENABLED` | `false` | เปิดใช้งานการเข้าสู่ระบบ SAML |
|
||||
| `SAML_IDP_SSO_URL` | | URL endpoint SSO ของ IdP **จำเป็น** เมื่อเปิดใช้งาน SAML |
|
||||
| `SAML_IDP_CERTIFICATE` | | ใบรับรองเซ็นชื่อ X.509 ของ IdP ในรูปแบบ PEM (ตัวข้อความใบรับรองเอง ไม่ใช่ path ของไฟล์) **จำเป็น** เมื่อเปิดใช้งาน SAML |
|
||||
| `EXTERNAL_URL` | | URL สาธารณะที่เข้าถึง SnapOtter ได้ **จำเป็น** เมื่อเปิดใช้งาน SAML |
|
||||
| `SAML_ENTITY_ID` | `${EXTERNAL_URL}/api/auth/saml/metadata` | SP Entity ID / Audience URI ที่ส่งไปยัง IdP |
|
||||
| `SAML_CALLBACK_URL` | `${EXTERNAL_URL}/api/auth/saml/callback` | URL ของ Assertion Consumer Service (ACS) |
|
||||
| `SAML_AUTO_CREATE_USERS` | `true` | สร้างบัญชีผู้ใช้ในเครื่องโดยอัตโนมัติเมื่อเข้าสู่ระบบ SAML ครั้งแรก |
|
||||
| `SAML_AUTO_LINK_USERS` | `false` | เชื่อมโยงข้อมูลประจำตัว SAML กับผู้ใช้ในเครื่องที่มีอยู่หากที่อยู่อีเมลตรงกัน |
|
||||
| `SAML_DEFAULT_ROLE` | `user` | บทบาทที่กำหนดให้ผู้ใช้ SAML ที่สร้างอัตโนมัติ หนึ่งใน `admin`, `editor` หรือ `user` |
|
||||
| `SAML_PROVIDER_NAME` | | ป้ายที่แสดงสำหรับปุ่มเข้าสู่ระบบ SAML บน frontend (เช่น "Okta", "Azure AD") หากว่างเปล่า ปุ่มจะแสดง "SAML" |
|
||||
| `SAML_USERNAME_ATTRIBUTE` | | แอตทริบิวต์ assertion ของ SAML ที่ใช้เป็นชื่อผู้ใช้ หากว่างเปล่า จะ fallback ไปที่ส่วน local-part ของอีเมล แล้วจึงเป็น NameID |
|
||||
| `SAML_EMAIL_ATTRIBUTE` | `email` | แอตทริบิวต์ assertion ของ SAML ที่ใช้เป็นที่อยู่อีเมลของผู้ใช้ |
|
||||
|
||||
เซิร์ฟเวอร์จะปฏิเสธการเริ่มทำงานหาก `SAML_ENABLED=true` และตัวแปรที่จำเป็นสามตัว (`SAML_IDP_SSO_URL`, `SAML_IDP_CERTIFICATE`, `EXTERNAL_URL`) ตัวใดตัวหนึ่งขาดหายไป
|
||||
|
||||
::: details หมายเหตุด้านความปลอดภัย
|
||||
ทั้ง `wantAuthnResponseSigned` และ `wantAssertionsSigned` ถูกกำหนดค่าตายตัวเป็น `true` SnapOtter ปฏิเสธการตอบกลับ SAML ที่ไม่ได้เซ็นชื่อหรือเซ็นชื่อไม่ถูกต้อง assertion จาก IdP ที่เชื่อถือได้จะถือว่าอีเมลได้รับการยืนยันแล้ว
|
||||
|
||||
รองรับเฉพาะการเข้าสู่ระบบที่ริเริ่มโดย SP เท่านั้น SnapOtter ไม่รองรับการเข้าสู่ระบบที่ริเริ่มโดย IdP (ไม่ได้ร้องขอ) หรือ Single Logout (SLO) การออกจากระบบของ SnapOtter จะไม่ทำให้ผู้ใช้ออกจากระบบของ IdP
|
||||
:::
|
||||
|
||||
## SP metadata และ 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`
|
||||
- Metadata endpoint: `https://photos.example.com/api/auth/saml/metadata` (คืนค่า XML)
|
||||
|
||||
IdP บางรายสามารถนำเข้า SP metadata 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** ให้เพิ่ม `email` ที่แมปไปยัง `user.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** และคลิก **Edit** ในส่วน **Basic SAML Configuration**:
|
||||
- **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` เป็น SSO URL จากขั้นตอนที่ 3 และ `SAML_IDP_CERTIFICATE` เป็นเนื้อหาใบรับรองที่ดาวน์โหลดมา
|
||||
|
||||
### SAML 2.0 IdP ทั่วไป {#generic-saml-2-0-idp}
|
||||
|
||||
สำหรับผู้ให้บริการข้อมูลประจำตัวที่รองรับ SAML 2.0 ใด ๆ:
|
||||
|
||||
1. สร้างแอปพลิเคชัน/service provider SAML ใหม่ใน IdP ของคุณ
|
||||
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** และ **ใบรับรองเซ็นชื่อ** ลงใน `SAML_IDP_SSO_URL` และ `SAML_IDP_CERTIFICATE`
|
||||
|
||||
## การจัดสรรผู้ใช้ {#user-provisioning}
|
||||
|
||||
### สร้างอัตโนมัติ {#auto-create}
|
||||
|
||||
เมื่อ `SAML_AUTO_CREATE_USERS` เป็น `true` (ค่าเริ่มต้น) บัญชีผู้ใช้ในเครื่องจะถูกสร้างขึ้นในครั้งแรกที่มีคนเข้าสู่ระบบผ่าน SAML บทบาทจะถูกตั้งเป็น `SAML_DEFAULT_ROLE`
|
||||
|
||||
ชื่อผู้ใช้จะได้มาตามลำดับนี้:
|
||||
|
||||
1. ค่าของแอตทริบิวต์ assertion ที่ระบุโดย `SAML_USERNAME_ATTRIBUTE` (หากตั้งค่าและมีอยู่)
|
||||
2. ส่วน local-part ของที่อยู่อีเมล (ทุกอย่างก่อน `@`)
|
||||
3. SAML NameID
|
||||
|
||||
หากเกิดชื่อผู้ใช้ซ้ำกัน จะมีการต่อท้ายด้วยตัวเลข (เช่น `jane` กลายเป็น `jane_2`)
|
||||
|
||||
### เชื่อมโยงอัตโนมัติ {#auto-link}
|
||||
|
||||
เมื่อ `SAML_AUTO_LINK_USERS` เป็น `true` SnapOtter จะเชื่อมโยงข้อมูลประจำตัว SAML กับบัญชีในเครื่องที่มีอยู่หากที่อยู่อีเมลตรงกัน สิ่งนี้มีประโยชน์เมื่อคุณมีบัญชีผู้ใช้ที่สร้างไว้ล่วงหน้าและต้องการให้พวกเขาเริ่มใช้ SSO โดยไม่สูญเสียข้อมูล
|
||||
|
||||
::: warning
|
||||
เปิดใช้งานการเชื่อมโยงอัตโนมัติเฉพาะเมื่อคุณไว้ใจ SAML IdP ของคุณในการตรวจสอบที่อยู่อีเมล อีเมลที่ไม่ได้รับการยืนยันจาก IdP ที่กำหนดค่าผิดพลาดอาจทำให้ใครบางคนเข้ายึดบัญชีของผู้ใช้อื่นได้
|
||||
:::
|
||||
|
||||
### การแมปแอตทริบิวต์ {#attribute-mapping}
|
||||
|
||||
| ฟิลด์ของ SnapOtter | แหล่งที่มา | การกำหนดค่า |
|
||||
|---|---|---|
|
||||
| Email | แอตทริบิวต์ assertion | `SAML_EMAIL_ATTRIBUTE` (ค่าเริ่มต้น: `email`) |
|
||||
| Username | แอตทริบิวต์ assertion, อีเมล หรือ 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 username**: นี่คือบัญชีในเครื่องเดียวที่ยังสามารถเข้าสู่ระบบด้วยรหัสผ่านได้ สำหรับการเข้าถึงฉุกเฉินหาก IdP ไม่สามารถเข้าถึงได้
|
||||
|
||||
เมื่อการบังคับใช้ SSO เปิดใช้งาน ความพยายามเข้าสู่ระบบในเครื่องใด ๆ (ยกเว้นสำหรับผู้ใช้ break-glass) จะคืนค่าข้อผิดพลาด 403 พร้อมข้อความ "Local password login is disabled. Please use SSO."
|
||||
|
||||
::: tip
|
||||
กำหนดค่า break-glass username เสมอก่อนเปิดใช้งานการบังคับใช้ SSO หากไม่มี คุณอาจถูกล็อกออกจาก SnapOtter หาก IdP ของคุณล่ม
|
||||
:::
|
||||
|
||||
## การใช้ 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 ล้มเหลว {#assertion-validation-failed}
|
||||
|
||||
ไม่สามารถตรวจสอบลายเซ็นการตอบกลับ SAML หรือลายเซ็น assertion ได้ ตรวจสอบ:
|
||||
|
||||
- ใบรับรองใน `SAML_IDP_CERTIFICATE` ตรงกับใบรับรองเซ็นชื่อปัจจุบันใน IdP ของคุณ (ใบรับรองมีการหมุนเวียน จึงควรตรวจสอบวันหมดอายุ)
|
||||
- ใบรับรองอยู่ในรูปแบบ PEM (ขึ้นต้นด้วย `-----BEGIN CERTIFICATE-----`)
|
||||
- ใบรับรองเป็นข้อความเต็ม ไม่ใช่ path ของไฟล์
|
||||
- ACS URL และ Entity ID ที่กำหนดค่าใน IdP ของคุณตรงกับค่าของ SnapOtter ทุกประการ (scheme, host, พอร์ต, path)
|
||||
|
||||
### แอตทริบิวต์ที่ขาดหายไป {#missing-attributes}
|
||||
|
||||
หากชื่อผู้ใช้หรืออีเมลว่างเปล่าหลังจากเข้าสู่ระบบ IdP ของคุณอาจไม่ได้ส่งแอตทริบิวต์ที่คาดหวัง ตรวจสอบ:
|
||||
|
||||
- IdP ของคุณถูกกำหนดค่าให้ปล่อยแอตทริบิวต์ `email` (หรือค่าใด ๆ ที่ `SAML_EMAIL_ATTRIBUTE` ถูกตั้งไว้)
|
||||
- หากใช้ `SAML_USERNAME_ATTRIBUTE` ตรวจสอบว่าแอตทริบิวต์นั้นรวมอยู่ใน assertion
|
||||
- IdP บางรายต้องการการกำหนดค่าการแมปแอตทริบิวต์อย่างชัดเจนก่อนที่จะปล่อย claim
|
||||
|
||||
### ความคลาดเคลื่อนของนาฬิกา {#clock-skew}
|
||||
|
||||
assertion ของ SAML มีเงื่อนไข timestamp (`NotBefore`, `NotOnOrAfter`) หากนาฬิกาเซิร์ฟเวอร์และนาฬิกา IdP ไม่ตรงกัน การตรวจสอบ assertion จะล้มเหลว รัน 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 ต้องการให้ทั้งการตอบกลับและ assertion ถูกเซ็นชื่อ)
|
||||
@@ -0,0 +1,298 @@
|
||||
---
|
||||
description: "ตั้งค่าการจัดสรร SCIM 2.0 เพื่อซิงก์ผู้ใช้และกลุ่มจากผู้ให้บริการข้อมูลประจำตัว (identity provider) ของคุณไปยัง SnapOtter ครอบคลุม Okta, Azure AD / Entra ID และการผสานรวมแบบกำหนดเอง"
|
||||
i18n_source_hash: bbd50119ec12
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 8629fd86555f
|
||||
---
|
||||
|
||||
# การจัดสรรด้วย SCIM {#scim-provisioning}
|
||||
|
||||
SnapOtter รองรับ SCIM 2.0 (System for Cross-domain Identity Management) สำหรับการจัดสรรผู้ใช้และกลุ่มแบบอัตโนมัติ ผู้ให้บริการข้อมูลประจำตัวของคุณสามารถสร้าง อัปเดต ปิดใช้งาน และเปิดใช้งานบัญชีผู้ใช้ใหม่ รวมถึงซิงก์การเป็นสมาชิกกลุ่มได้โดยอัตโนมัติ
|
||||
|
||||
::: tip คุณสมบัติสำหรับองค์กร (Enterprise)
|
||||
การจัดสรรด้วย SCIM ต้องใช้ไลเซนส์ **enterprise** ที่มีคุณสมบัติ `scim` ไม่มีให้ใช้งานในแผน team หากไม่มีคุณสมบัตินี้ ปลายทาง SCIM ทั้งหมด (ยกเว้น discovery) จะคืนค่า 403
|
||||
:::
|
||||
|
||||
## ข้อกำหนดเบื้องต้น {#prerequisites}
|
||||
|
||||
- อินสแตนซ์ SnapOtter ที่ทำงานอยู่และเข้าถึงได้ผ่าน URL สาธารณะ
|
||||
- คีย์ไลเซนส์ enterprise ที่มีคุณสมบัติ `scim`
|
||||
- สิทธิ์ผู้ดูแลระบบใน SnapOtter (ต้องมีสิทธิ์ `users:manage` เพื่อสร้างหรือเพิกถอนโทเคน SCIM)
|
||||
- สิทธิ์ผู้ดูแลระบบในการตั้งค่าการจัดสรรของผู้ให้บริการข้อมูลประจำตัวของคุณ
|
||||
|
||||
## เริ่มต้นใช้งานอย่างรวดเร็ว {#quick-start}
|
||||
|
||||
1. สร้างโทเคน bearer สำหรับ SCIM:
|
||||
|
||||
```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 | Team | ได้ | ได้ | ได้ | ได้ |
|
||||
|
||||
::: warning
|
||||
Groups ใน SCIM แมปกับ **teams** ของ SnapOtter ไม่ใช่บทบาท (roles) SCIM ไม่สามารถตั้งค่าบทบาทของผู้ใช้ได้ ผู้ใช้ทั้งหมดที่สร้างผ่าน SCIM จะได้รับบทบาท `user` หากต้องการเปลี่ยนบทบาทของผู้ใช้ ให้ใช้ UI ผู้ดูแลระบบของ SnapOtter
|
||||
:::
|
||||
|
||||
## การดำเนินการกับผู้ใช้ {#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`
|
||||
|
||||
คืนค่ารายการผู้ใช้แบบแบ่งหน้า รองรับพารามิเตอร์ query `startIndex` และ `count` (สูงสุด 200 ผลลัพธ์ต่อหน้า)
|
||||
|
||||
การกรองรองรับเฉพาะ `eq` (เท่ากับ) เท่านั้น บนแอตทริบิวต์เหล่านี้:
|
||||
|
||||
- `userName eq "jane"`
|
||||
- `externalId eq "ext-12345"`
|
||||
|
||||
ตัวดำเนินการและแอตทริบิวต์การกรองอื่น ๆ จะคืนค่า HTTP 400
|
||||
|
||||
### รับข้อมูลผู้ใช้ {#get-user}
|
||||
|
||||
`GET /api/v1/scim/v2/Users/:id`
|
||||
|
||||
คืนค่าผู้ใช้รายเดียวตาม ID ผู้ใช้ SnapOtter ของพวกเขา
|
||||
|
||||
### แทนที่ผู้ใช้ {#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 การดำเนินการที่รองรับ:
|
||||
|
||||
| การดำเนินการ | Paths |
|
||||
|---|---|
|
||||
| `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}
|
||||
|
||||
หากต้องการเปิดใช้งานผู้ใช้ที่เคยถูกปิดใช้งานไปแล้วอีกครั้ง ให้ส่งคำขอ `PUT` หรือ `PATCH` พร้อม `active: true` 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}
|
||||
|
||||
Groups ใน SCIM แมปกับทีมของ 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`
|
||||
|
||||
รองรับการดำเนินการเหล่านี้:
|
||||
|
||||
| การดำเนินการ | Path | ผลลัพธ์ |
|
||||
|---|---|---|
|
||||
| `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**: โทเคน bearer สำหรับ SCIM ที่สร้างไว้ข้างต้น
|
||||
4. คลิก **Test API Credentials** แล้วคลิก **Save**
|
||||
5. ภายใต้ **Provisioning > To App** ให้เปิดใช้งาน:
|
||||
- **Create Users**
|
||||
- **Update User Attributes**
|
||||
- **Deactivate Users**
|
||||
6. ภายใต้ **Push Groups** ให้กำหนดค่าว่ากลุ่ม Okta ใดที่จะซิงก์เป็นทีมของ SnapOtter
|
||||
|
||||
### 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**: โทเคน bearer สำหรับ SCIM ที่สร้างไว้ข้างต้น
|
||||
4. คลิก **Test Connection** แล้วคลิก **Save**
|
||||
5. ภายใต้ **Mappings** ให้กำหนดค่าการแมปแอตทริบิวต์ของผู้ใช้และกลุ่ม โดยปกติค่าเริ่มต้นจะใช้งานได้ แต่ให้ตรวจสอบว่า `userName` แมปกับ `userPrincipalName` หรือ `mail` ตามที่ต้องการ
|
||||
6. ตั้ง **Provisioning Status** เป็น **On** แล้วบันทึก
|
||||
|
||||
Azure จัดสรรผู้ใช้และกลุ่มตามรอบการซิงก์ที่กำหนดไว้ (โดยทั่วไปทุก ๆ 40 นาที)
|
||||
|
||||
## ปลายทางสำหรับ Discovery {#discovery-endpoints}
|
||||
|
||||
ปลายทางทั้งสามนี้ใช้งานได้โดยไม่ต้องยืนยันตัวตน และอธิบายความสามารถของเซิร์ฟเวอร์ SCIM:
|
||||
|
||||
| ปลายทาง | คำอธิบาย |
|
||||
|---|---|
|
||||
| `GET /api/v1/scim/v2/ServiceProviderConfig` | ความสามารถของเซิร์ฟเวอร์และคุณสมบัติที่รองรับ |
|
||||
| `GET /api/v1/scim/v2/Schemas` | คำนิยาม schema ของ 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) ไม่ได้ถูกนำมาใช้งาน
|
||||
- **การดำเนินการแบบ Bulk**: ไม่รองรับ
|
||||
- **Sort และ ETag**: ไม่รองรับ
|
||||
- **บทบาท**: SCIM ไม่สามารถกำหนดบทบาทของ SnapOtter ได้ ผู้ใช้ที่จัดสรรทั้งหมดจะได้รับบทบาท `user`
|
||||
- **MAX_USERS**: ขีดจำกัดของตัวแปรสภาพแวดล้อม `MAX_USERS` ไม่ได้ถูกบังคับใช้เมื่อสร้างผู้ใช้ผ่าน SCIM หากคุณต้องการจำกัดจำนวนผู้ใช้ ให้จัดการการมอบหมายใน IdP ของคุณ
|
||||
- **หนึ่งโทเคน**: มีโทเคน SCIM ที่ใช้งานได้เพียงหนึ่งโทเคนในแต่ละครั้ง หาก IdP หลายรายต้องเข้าถึง SCIM พวกเขาต้องใช้โทเคนร่วมกัน
|
||||
- **กลุ่มคือทีม**: Groups ใน SCIM สอดคล้องกับทีม ไม่ใช่บทบาทหรือกลุ่มสิทธิ์
|
||||
|
||||
## การแก้ไขปัญหา {#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 ต้องใช้ไลเซนส์แผน enterprise ตรวจสอบว่าตั้งค่า `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}
|
||||
|
||||
DELETE ของ SCIM คือการปิดใช้งานแบบซอฟต์ ผู้ใช้ที่ถูกปิดใช้งานจะยังคงปรากฏในรายการผู้ใช้ของผู้ดูแลระบบพร้อมสถานะปิดใช้งาน นี่เป็นการออกแบบเพื่อให้ข้อมูลของพวกเขาถูกเก็บรักษาไว้ บทบาทของพวกเขาจะแสดงเป็น `disabled:<original-role>`
|
||||
@@ -0,0 +1,339 @@
|
||||
---
|
||||
description: "คู่มือการเสริมความแข็งแกร่งด้านความปลอดภัยสำหรับ SnapOtter ความปลอดภัยของคอนเทนเนอร์ การแยกเครือข่าย Docker secrets การปรับใช้ Kubernetes และอาร์ทิแฟกต์ด้านการปฏิบัติตามข้อกำหนด"
|
||||
i18n_source_hash: 986f7658430c
|
||||
i18n_provenance: machine
|
||||
i18n_output_hash: 3c4f44580e5e
|
||||
---
|
||||
|
||||
# Security & Hardening {#security-hardening}
|
||||
|
||||
SnapOtter ประมวลผลไฟล์ทั้งหมดบนโครงสร้างพื้นฐานของคุณ มันส่งการวิเคราะห์ผลิตภัณฑ์และรายงานการล่มแบบไม่ระบุตัวตนที่ไม่มีเนื้อหาโดยค่าเริ่มต้น เพื่อช่วยปรับปรุงโครงการ มันไม่เคยส่งไฟล์, ชื่อไฟล์, เนื้อหาไฟล์, เอาต์พุต OCR, เมตาดาตาของรูปภาพ หรือข้อความในเอกสารของคุณ ข้อเสนอแนะเสริมจะถูกส่งหลังจากผู้ใช้ส่งเท่านั้น เฉพาะเมื่อเปิดใช้การวิเคราะห์ และฟิลด์ข้อมูลติดต่อจะรวมอยู่ด้วยเฉพาะเมื่อมีความยินยอมด้านการติดต่ออย่างชัดเจน ผู้ดูแลระบบสามารถปิดการวิเคราะห์และการเก็บข้อเสนอแนะได้ในคลิกเดียวภายใต้ Settings > System > Privacy โดยไม่ต้อง build ใหม่ การประมวลผลไฟล์อยู่ภายในคอนเทนเนอร์ของคุณเสมอ
|
||||
|
||||
คอนเทนเนอร์รันเป็นผู้ใช้ที่ไม่ใช่ root โดยเฉพาะ (`snapotter`) โดยตัด Linux capabilities ทั้งหมดออก ยกเว้นชุดขั้นต่ำที่จำเป็น สำหรับนโยบายการเปิดเผยช่องโหว่ฉบับเต็มและสถาปัตยกรรมความปลอดภัย ดู [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md) บน GitHub
|
||||
|
||||
## Container Hardening {#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:
|
||||
```
|
||||
|
||||
### Why `no-new-privileges` Is Not Set {#why-no-new-privileges-is-not-set}
|
||||
|
||||
`security_opt: [no-new-privileges:true]` ถูกละไว้โดยตั้งใจ entrypoint เริ่มเป็น root เพื่อแก้ไขความเป็นเจ้าของของวอลุ่ม จากนั้นลดสิทธิ์เป็นผู้ใช้ `snapotter` ผ่าน [gosu](https://github.com/tianon/gosu) ซึ่งต้องใช้ setuid เมื่อการลดสิทธิ์เสร็จสมบูรณ์ กระบวนการจะรันเป็น `snapotter` โดยตัด capabilities ทั้งหมดออก ยกเว้นห้ารายการที่ระบุไว้ด้านบน
|
||||
|
||||
หากคุณใช้ Kubernetes หรือแฟล็ก `--user` ของ Docker เพื่อรันเป็น non-root โดยตรง (ข้าม gosu) การเปิดใช้ `no-new-privileges` ก็ปลอดภัย
|
||||
|
||||
### Why `read_only` Is Not Set {#why-read-only-is-not-set}
|
||||
|
||||
`read_only: true` ไม่ได้ถูกตั้งค่า เพราะการรีแมป PUID/PGID เขียนลง `/etc/passwd` และ `/etc/group` ตอนเริ่มทำงาน หากคุณใช้แฟล็ก `--user` ของ Docker หรือ `runAsUser` ของ Kubernetes แทน PUID/PGID คุณสามารถเปิดใช้ระบบไฟล์รากแบบอ่านอย่างเดียวได้อย่างปลอดภัย
|
||||
|
||||
## Network Isolation {#network-isolation}
|
||||
|
||||
ระหว่างการทำงานปกติ คอนเทนเนอร์ทำการเชื่อมต่อเครือข่ายขาออก **เป็นศูนย์** การประมวลผลไฟล์ทั้งหมดเกิดขึ้นในเครื่องโดยใช้ไลบรารีที่มาพร้อมกับตัวโปรแกรม
|
||||
|
||||
```
|
||||
Browser --> Reverse Proxy (TLS) --> SnapOtter container --> (nothing)
|
||||
```
|
||||
|
||||
ข้อยกเว้นเดียวคือ **การดาวน์โหลดโมเดล AI**: เมื่อผู้ใช้ติดตั้งบันเดิลฟีเจอร์ AI ผ่าน UI คอนเทนเนอร์จะดาวน์โหลดไฟล์เก็บบันเดิลที่สร้างไว้ล่วงหน้าจาก Hugging Face รวมถึงไฟล์โมเดลแต่ละไฟล์อีกไม่กี่ไฟล์จาก GitHub Releases, Google Storage และ PyPI การดาวน์โหลดเหล่านี้เกิดขึ้นครั้งเดียวต่อบันเดิลและเก็บไว้ในวอลุ่ม `/data`
|
||||
|
||||
**คำแนะนำเรื่องไฟร์วอลล์:**
|
||||
|
||||
| สถานการณ์ | กฎขาออก |
|
||||
|---|---|
|
||||
| Air-gapped (ไม่มี AI) | บล็อกทราฟฟิกขาออกทั้งหมดจากคอนเทนเนอร์ |
|
||||
| ต้องการบันเดิล AI | อนุญาต HTTPS ไปยัง `huggingface.co`, `*.xethub.hf.co`, `cdn-lfs.huggingface.co`, `github.com`, `objects.githubusercontent.com`, `storage.googleapis.com`, `pypi.org`, `files.pythonhosted.org` ระหว่างการติดตั้ง จากนั้นบล็อก |
|
||||
| หลังติดตั้ง AI | บล็อกทราฟฟิกขาออกทั้งหมด โมเดลถูกแคชไว้ในเครื่องแล้ว |
|
||||
|
||||
ไฟล์เก็บบันเดิลถูกให้บริการจาก Xet storage ของ Hugging Face ซึ่งถ่ายโอนผ่าน endpoint `*.xethub.hf.co` แบบขนาน และเป็นสิ่งที่ทำให้การดาวน์โหลดบันเดิลขนาดหลาย GB รวดเร็ว หากไฟร์วอลล์ของคุณอนุญาต `huggingface.co` แต่บล็อก `*.xethub.hf.co` การติดตั้งยังคงสำเร็จ แต่จะสำรองไปใช้การดาวน์โหลดแบบสตรีมเดียวที่ช้ากว่า ดังนั้นให้ allowlist โฮสต์ Xet ไว้เพื่อคงอยู่ในเส้นทางที่เร็ว การติดตั้งแบบออฟไลน์ทั้งหมดสามารถข้ามทั้งหมดนี้และใช้ [Offline Bundle Import](/th/guide/deployment) แทนได้
|
||||
|
||||
สำหรับการกำหนดค่า reverse proxy (Nginx, Traefik, Caddy, Cloudflare Tunnels) ดู [คู่มือ Deployment](/th/guide/deployment#reverse-proxy)
|
||||
|
||||
## Docker Secrets {#docker-secrets}
|
||||
|
||||
สำหรับการปรับใช้ในโปรดักชัน ควรหลีกเลี่ยงการส่งความลับเป็นตัวแปรสภาพแวดล้อมแบบข้อความธรรมดา entrypoint รองรับข้อกำหนด `_FILE` ของ Docker: เมานต์ความลับเป็นไฟล์แล้วตั้งค่าตัวแปร `_FILE` ที่สอดคล้องกันให้เป็นพาธของมัน
|
||||
|
||||
**ความลับที่รองรับ:**
|
||||
|
||||
| ตัวแปร | `_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 Deployment {#kubernetes-deployment}
|
||||
|
||||
entrypoint ตรวจจับเมื่อคอนเทนเนอร์รันเป็น non-root อยู่แล้ว (เช่น ผ่าน `runAsUser` ของ Kubernetes) และข้ามการลดสิทธิ์ด้วย gosu โดยอัตโนมัติ ในกรณีนั้นมันไม่สามารถ chown วอลุ่มที่เมานต์ได้เอง ดังนั้นมันจะตรวจสอบว่าวอลุ่มเขียนได้ และออกก่อนกำหนดพร้อมคำแนะนำที่นำไปปฏิบัติได้หากเขียนไม่ได้ ดู [Storage permissions](/th/guide/deployment#storage-permissions) สำหรับ `fsGroup` และการตั้งค่าแบบ UID ต่างถิ่น (TrueNAS, OpenShift)
|
||||
|
||||
**SecurityContext ของ Pod ที่แนะนำ:**
|
||||
|
||||
```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 entrypoint จึงข้าม gosu ทั้งหมด สิ่งนี้อนุญาต capabilities `allowPrivilegeEscalation: false` และ `drop: [ALL]` โดยไม่ขัดแย้งกัน
|
||||
|
||||
สำหรับการกำหนดขนาดทรัพยากร ดู [Hardware Requirements](/th/guide/deployment#hardware-requirements)
|
||||
|
||||
## Backup and Recovery {#backup-and-recovery}
|
||||
|
||||
สถานะถาวรถูกแบ่งออกเป็นสองวอลุ่ม:
|
||||
|
||||
| วอลุ่ม | เนื้อหา | สำคัญไหม? |
|
||||
|---|---|---|
|
||||
| `SnapOtter-pgdata` | ฐานข้อมูล PostgreSQL (ผู้ใช้, การตั้งค่า, ไปป์ไลน์, งาน, บันทึกการตรวจสอบ) | ใช่ |
|
||||
| `/data` (วอลุ่มแอป) | ไฟล์ที่ผู้ใช้อัปโหลด, โมเดล AI, Python venv | บางส่วน (ดูด้านล่าง) |
|
||||
|
||||
ภายในวอลุ่ม `/data`:
|
||||
|
||||
| พาธ | เนื้อหา | สำคัญไหม? |
|
||||
|---|---|---|
|
||||
| `/data/uploads/`, `/data/outputs/` | ไฟล์ผู้ใช้และผลการประมวลผล | ใช่ |
|
||||
| `/data/ai/` | ไฟล์โมเดล AI ที่ดาวน์โหลด | ไม่ (ดาวน์โหลดใหม่ได้) |
|
||||
| `/data/venv/` | Python virtual environment | ไม่ (สร้างใหม่ตอนเริ่ม) |
|
||||
|
||||
### Database backup {#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
|
||||
```
|
||||
|
||||
หรืออีกทางเลือกหนึ่ง ให้หยุดสแตกและ snapshot วอลุ่ม `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 {#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 {#compliance-artifacts}
|
||||
|
||||
แต่ละรีลีสของ SnapOtter มีอาร์ทิแฟกต์ด้านความปลอดภัยต่อไปนี้:
|
||||
|
||||
| อาร์ทิแฟกต์ | รูปแบบ | หาได้ที่ไหน |
|
||||
|---|---|---|
|
||||
| SBOM (CycloneDX) | JSON | asset ของ [GitHub Release](https://github.com/snapotter-hq/SnapOtter/releases): `snapotter-v{version}-sbom.cdx.json` |
|
||||
| SBOM (SPDX) | JSON | asset ของ [GitHub Release](https://github.com/snapotter-hq/SnapOtter/releases): `snapotter-v{version}-sbom.spdx.json` |
|
||||
| การสแกนช่องโหว่ | Trivy JSON | asset ของ [GitHub Release](https://github.com/snapotter-hq/SnapOtter/releases): `snapotter-v{version}-trivy.json` |
|
||||
| การสแกนช่องโหว่ | SARIF | แท็บ [GitHub Security](https://github.com/snapotter-hq/SnapOtter/security) |
|
||||
| การวิเคราะห์แบบสถิต | CodeQL (JS/TS + Python) | แท็บ [GitHub Security](https://github.com/snapotter-hq/SnapOtter/security), รันรายสัปดาห์ + ต่อ PR |
|
||||
| การตรวจสอบ dependency | GitHub native | การตรวจสอบต่อ PR ล้มเหลวเมื่อมีการเพิ่มความรุนแรงสูง |
|
||||
| การตรวจสอบ dependency ของ Python | pip-audit | ล็อกการรัน CI ในทุก push |
|
||||
| นโยบายความปลอดภัย | Markdown | [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md) ในที่เก็บ |
|
||||
| การอัปเดต dependency | Dependabot | PR อัตโนมัติรายสัปดาห์สำหรับ npm, pip, Docker, Actions |
|
||||
|
||||
**การรันสแกนของคุณเอง:**
|
||||
|
||||
ดาวน์โหลด SBOM จากรีลีสและสแกนด้วยเครื่องมือที่คุณต้องการ:
|
||||
|
||||
```bash
|
||||
# Scan with Grype using the CycloneDX SBOM
|
||||
grype sbom:snapotter-v1.17.2-sbom.cdx.json
|
||||
|
||||
# Scan with Trivy using the SPDX SBOM
|
||||
trivy sbom snapotter-v1.17.2-sbom.spdx.json
|
||||
|
||||
# Scan the Docker image directly
|
||||
trivy image snapotter/snapotter:1.17.2
|
||||
```
|
||||
|
||||
::: info
|
||||
SBOM และการสแกนช่องโหว่สะท้อนอิมเมจที่เผยแพร่สำหรับรีลีสนั้นอย่างแน่นอน บันเดิลโมเดล AI ที่ติดตั้งหลังการปรับใช้ไม่ได้รวมอยู่ใน SBOM เนื่องจากดาวน์โหลดขณะรันไทม์
|
||||
:::
|
||||
@@ -0,0 +1,239 @@
|
||||
---
|
||||
description: "รูปแบบไฟล์ที่รองรับในทุกโมดัลลิตี - รูปแบบอินพุตรูปภาพ 55+ รูปแบบ, วิดีโอ, เสียง, PDF และรูปแบบไฟล์"
|
||||
i18n_source_hash: e53ecf65be25
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 22bb1a231aab
|
||||
---
|
||||
|
||||
# รูปแบบที่รองรับ {#supported-formats}
|
||||
|
||||
SnapOtter ประมวลผลไฟล์ในห้าโมดัลลิตี ได้แก่ รูปภาพ วิดีโอ เสียง PDF และไฟล์ หน้านี้แสดงรายการรูปแบบที่รองรับทั้งหมด
|
||||
|
||||
## รูปแบบรูปภาพ {#image-formats}
|
||||
|
||||
SnapOtter รองรับรูปแบบรูปภาพ 55+ รูปแบบสำหรับอินพุต และ 13 รูปแบบสำหรับเอาต์พุต
|
||||
|
||||
## รูปแบบอินพุต {#input-formats}
|
||||
|
||||
### มาตรฐานเว็บ (9) {#web-standards-9}
|
||||
|
||||
| รูปแบบ | นามสกุล | ตัวถอดรหัส | หมายเหตุ |
|
||||
|--------|-----------|---------|-------|
|
||||
| JPEG | .jpg, .jpeg | Sharp (native) | |
|
||||
| PNG | .png | Sharp (native) | สกัดเฟรมแรกของ APNG |
|
||||
| WebP | .webp | Sharp (native) | |
|
||||
| GIF | .gif | Sharp (native) | รองรับแบบเคลื่อนไหว |
|
||||
| AVIF | .avif | Sharp (native) | |
|
||||
| SVG | .svg | Sharp (librsvg) | ทำให้ปลอดภัยจาก XXE/SSRF |
|
||||
| SVGZ | .svgz | gunzip + Sharp | ป้องกัน Gzip bomb |
|
||||
| APNG | .apng | Sharp (native) | เฉพาะเฟรมแรก |
|
||||
| JPEG XL | .jxl | djxl / ImageMagick | สำรองสองระดับ |
|
||||
|
||||
### ระดับมืออาชีพ (7) {#professional-7}
|
||||
|
||||
| รูปแบบ | นามสกุล | ตัวถอดรหัส | หมายเหตุ |
|
||||
|--------|-----------|---------|-------|
|
||||
| TIFF | .tiff, .tif | Sharp (native) | รองรับหลายหน้า |
|
||||
| PSD | .psd | ImageMagick | รวมเลเยอร์แบบแบน |
|
||||
| EPS | .eps, .epsf | ImageMagick + Ghostscript | แรสเตอร์ที่ 300dpi, เสริมความปลอดภัย |
|
||||
| OpenEXR | .exr | ImageMagick | แปลง Linear เป็น sRGB |
|
||||
| Radiance HDR | .hdr | ImageMagick | แปลง Linear เป็น sRGB |
|
||||
| DPX | .dpx | ImageMagick | แปลง Log เป็น sRGB |
|
||||
| Cineon | .cin | ImageMagick | รูปแบบ Film/VFX |
|
||||
|
||||
### กล้อง RAW (23) {#camera-raw-23}
|
||||
|
||||
| รูปแบบ | นามสกุล | แบรนด์กล้อง | ตัวถอดรหัส |
|
||||
|--------|-----------|-------------|---------|
|
||||
| DNG | .dng | Adobe (สากล) | exiftool / ImageMagick + LibRaw |
|
||||
| CR2 | .cr2 | Canon (ก่อนปี 2018) | exiftool / ImageMagick + LibRaw |
|
||||
| CR3 | .cr3 | Canon (2018+) | exiftool / ImageMagick + LibRaw |
|
||||
| NEF | .nef | Nikon | exiftool / ImageMagick + LibRaw |
|
||||
| NRW | .nrw | Nikon (Coolpix) | exiftool / ImageMagick + LibRaw |
|
||||
| ARW | .arw | Sony | exiftool / ImageMagick + LibRaw |
|
||||
| ORF | .orf | Olympus | exiftool / ImageMagick + LibRaw |
|
||||
| RW2 | .rw2 | Panasonic | exiftool / ImageMagick + LibRaw |
|
||||
| RAF | .raf | Fujifilm | exiftool / ImageMagick + LibRaw |
|
||||
| PEF | .pef | Pentax/Ricoh | exiftool / ImageMagick + LibRaw |
|
||||
| 3FR | .3fr | Hasselblad | exiftool / ImageMagick + LibRaw |
|
||||
| IIQ | .iiq | Phase One | exiftool / ImageMagick + LibRaw |
|
||||
| SRW | .srw | Samsung | exiftool / ImageMagick + LibRaw |
|
||||
| X3F | .x3f | Sigma | exiftool / ImageMagick + LibRaw |
|
||||
| RWL | .rwl | Leica | exiftool / ImageMagick + LibRaw |
|
||||
| GPR | .gpr | GoPro | exiftool / ImageMagick + LibRaw |
|
||||
| FFF | .fff | Hasselblad (รุ่นเก่า) | exiftool / ImageMagick + LibRaw |
|
||||
| MRW | .mrw | Minolta | exiftool / ImageMagick + LibRaw |
|
||||
| MEF | .mef | Mamiya | exiftool / ImageMagick + LibRaw |
|
||||
| KDC | .kdc | Kodak | exiftool / ImageMagick + LibRaw |
|
||||
| DCR | .dcr | Kodak | exiftool / ImageMagick + LibRaw |
|
||||
| ERF | .erf | Epson | exiftool / ImageMagick + LibRaw |
|
||||
| PTX | .ptx | Pentax (compact) | 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 (native) | pixmap แบบสี |
|
||||
| PGM | .pgm | Sharp (native) | โทนสีเทา |
|
||||
| PBM | .pbm | Sharp (native) | bitmap 1 บิต |
|
||||
| PNM | .pnm | Sharp (native) | รูปแบบครอบคลุม |
|
||||
| PAM | .pam | Sharp (native) | แผนที่ตามอำเภอใจ |
|
||||
| PFM | .pfm | Sharp (native) | แผนที่แบบ float |
|
||||
|
||||
## รูปแบบเอาต์พุต (13) {#output-formats-13}
|
||||
|
||||
| รูปแบบ | ตัวเข้ารหัส | การควบคุมคุณภาพ | มีในเครื่องมือ |
|
||||
|--------|---------|----------------|-------------|
|
||||
| JPEG | Sharp native | 1-100 | ทุกเครื่องมือ |
|
||||
| PNG | Sharp native | การบีบอัด 0-9 | ทุกเครื่องมือ |
|
||||
| WebP | Sharp native | 1-100 | ทุกเครื่องมือ |
|
||||
| AVIF | Sharp native | 1-100 | ทุกเครื่องมือ |
|
||||
| TIFF | Sharp native | 1-100 | เครื่องมือแปลงเต็มรูปแบบ |
|
||||
| GIF | Sharp native | 1-100 | เครื่องมือแปลงเต็มรูปแบบ |
|
||||
| JXL | Sharp native | 1-100 | ทุกเครื่องมือ |
|
||||
| HEIC | heif-enc CLI | 1-100 | เครื่องมือแปลงเต็มรูปแบบ |
|
||||
| HEIF | heif-enc CLI | 1-100 | เครื่องมือแปลงเต็มรูปแบบ |
|
||||
| BMP | ImageMagick CLI | ไม่สูญเสียคุณภาพ | เครื่องมือแปลง |
|
||||
| ICO | ImageMagick CLI | ไม่สูญเสียคุณภาพ | เครื่องมือแปลง |
|
||||
| JP2 | opj_compress CLI | อัตราการบีบอัด | เครื่องมือแปลง |
|
||||
| QOI | ตัวเข้ารหัสแบบอินไลน์ | ไม่สูญเสียคุณภาพ | เครื่องมือแปลง |
|
||||
|
||||
## รูปแบบวิดีโอ {#video-formats}
|
||||
|
||||
การถอดรหัสและเข้ารหัสวิดีโอจัดการโดย FFmpeg (static build) ดังนั้นทุกคอนเทนเนอร์และโคเดกทั่วไปจึงรองรับในอินพุต
|
||||
|
||||
### คอนเทนเนอร์อินพุต (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 | ชนิดย่อย MP4 ของ Apple |
|
||||
| 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 | แปลง, บีบอัด และเครื่องมือวิดีโอส่วนใหญ่ |
|
||||
| QuickTime | .mov | H.264 | แปลงวิดีโอ |
|
||||
| WebM | .webm | VP9 | แปลงวิดีโอ |
|
||||
| GIF | .gif | - | วิดีโอเป็น GIF |
|
||||
| WebP | .webp | - | วิดีโอเป็น WebP (แบบเคลื่อนไหว) |
|
||||
|
||||
### คำบรรยาย {#subtitles}
|
||||
|
||||
| รูปแบบ | นามสกุล | การดำเนินการ |
|
||||
|--------|-----------|-----------|
|
||||
| SubRip | .srt | ฝัง, เบิร์นอิน, สกัด, สร้างอัตโนมัติ |
|
||||
| WebVTT | .vtt | ฝัง, เบิร์นอิน, สกัด, สร้างอัตโนมัติ |
|
||||
| ASS / SSA | .ass | ฝัง, เบิร์นอิน (รองรับการจัดรูปแบบ) |
|
||||
|
||||
## รูปแบบเสียง {#audio-formats}
|
||||
|
||||
เสียงก็ประมวลผลโดย 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 | แปลงเสียง, สกัดเสียง |
|
||||
| WAV | .wav | PCM | แปลงเสียง, สกัดเสียง |
|
||||
| FLAC | .flac | FLAC (ไม่สูญเสียคุณภาพ) | แปลงเสียง |
|
||||
| Ogg | .ogg | Vorbis | แปลงเสียง |
|
||||
| M4A | .m4a | AAC | แปลงเสียง, สกัดเสียง |
|
||||
|
||||
## รูปแบบเอกสาร {#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 | รูปแบบ E-book |
|
||||
|
||||
### รูปแบบเอาต์พุต {#output-formats-2}
|
||||
|
||||
| รูปแบบ | นามสกุล | สร้างโดย |
|
||||
|--------|-----------|-------------|
|
||||
| PDF | .pdf | Word/Excel/PowerPoint เป็น PDF, Markdown เป็น PDF, HTML เป็น PDF |
|
||||
| PDF/A | .pdf | แปลง PDF/A (เก็บถาวร) |
|
||||
| Word | .docx, .odt, .rtf, .txt | แปลงเอกสาร, PDF เป็น Word, Markdown เป็น Word |
|
||||
| Presentation | .pptx, .odp | แปลงงานนำเสนอ |
|
||||
| Spreadsheet | .xlsx, .ods, .csv | แปลงสเปรดชีต |
|
||||
| HTML | .html | Markdown เป็น HTML |
|
||||
| EPUB | .epub | แปลงเป็น EPUB |
|
||||
| Images | .png, .jpg | PDF เป็นรูปภาพ |
|
||||
|
||||
## รูปแบบไฟล์ {#file-formats}
|
||||
|
||||
เครื่องมือข้อมูลและไฟล์บีบอัดจะแปลงระหว่างรูปแบบที่มีโครงสร้างและรวมไฟล์เข้าด้วยกัน
|
||||
|
||||
| รูปแบบ | นามสกุล | การแปลง |
|
||||
|--------|-----------|-------------|
|
||||
| CSV | .csv | เป็น/จาก JSON และ Excel; แยกและรวม; จาก XML |
|
||||
| JSON | .json | เป็น/จาก CSV, XML และ YAML |
|
||||
| XML | .xml | เป็น/จาก JSON; เป็น CSV |
|
||||
| YAML | .yaml, .yml | เป็น/จาก JSON |
|
||||
| Excel | .xlsx | เป็น/จาก CSV |
|
||||
| ZIP | .zip | สร้างไฟล์บีบอัด, สกัดเนื้อหา |
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
description: "SnapOtter เก็บข้อมูลการใช้งานแบบไม่ระบุตัวตนอะไรบ้าง ส่งเมื่อใด และวิธีปิดการวิเคราะห์ผลิตภัณฑ์ทั่วทั้งอินสแตนซ์"
|
||||
i18n_source_hash: 5d72dedaeb23
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: f748e58cdbd8
|
||||
---
|
||||
|
||||
# SnapOtter เก็บข้อมูลอะไรบ้าง {#what-snapotter-collects}
|
||||
|
||||
การวิเคราะห์ผลิตภัณฑ์แบบไม่ระบุตัวตนเปิดใช้งานตามค่าเริ่มต้น และถูกตั้งค่าสำหรับทั้งอินสแตนซ์โดยผู้ดูแลระบบ ปิดได้ภายใต้ Settings > System > Privacy
|
||||
|
||||
## เหตุการณ์ที่เราส่ง (เมื่อเปิดใช้งาน) {#events-we-send-when-enabled}
|
||||
|
||||
- tool_used: id เครื่องมือ, สถานะ, ระยะเวลา, หมวดหมู่, เป็นเครื่องมือ AI หรือไม่, รหัสข้อผิดพลาดเมื่อล้มเหลว
|
||||
- pipeline_executed: จำนวนขั้นตอน, id เครื่องมือ, แฟล็ก batch, จำนวนไฟล์, ระยะเวลา, สถานะ
|
||||
- ai_bundle_action: id ชุด, การกระทำ, ระยะเวลา
|
||||
- การใช้งานฟรอนต์เอนด์: หน้าเครื่องมือใดที่เปิด, ไฟล์ที่เพิ่ม (นับจำนวนเท่านั้น), เครื่องมือที่เริ่มใช้, การดาวน์โหลด, การบันทึก, การค้นหา (นับจำนวนผลลัพธ์เท่านั้น), การประมวลผลแบบ batch
|
||||
- รายงานข้อขัดข้อง: ประเภทข้อผิดพลาดและ source stack ที่มีเฉพาะชื่อฐานของไฟล์เท่านั้น
|
||||
|
||||
## สิ่งที่เราไม่เก็บเด็ดขาด {#what-we-never-collect}
|
||||
|
||||
- ชื่อไฟล์หรือพาธ
|
||||
- เนื้อหาไฟล์
|
||||
- ข้อความผลลัพธ์ OCR
|
||||
- เมทาดาทาของรูปภาพ (EXIF)
|
||||
- ข้อความที่สกัดจากเอกสาร
|
||||
- ที่อยู่ IP หรือข้อมูลระบุตัวตนของบัญชีคุณ
|
||||
|
||||
## การปิดใช้งาน {#turning-it-off}
|
||||
|
||||
ผู้ดูแลระบบ: Settings > System > Privacy สลับ "Anonymous Product Analytics" ให้ปิด มันจะหยุดทันทีทั่วทั้งอินสแตนซ์ หากต้องการสร้างอิมเมจที่ไม่สามารถส่งข้อมูลได้เลย ให้ตั้งค่า build arg `SNAPOTTER_ANALYTICS=off`
|
||||
@@ -0,0 +1,234 @@
|
||||
---
|
||||
description: "21 ภาษาที่รองรับ และวิธีสร้างหรือปรับปรุงคำแปลสำหรับ SnapOtter โดยใช้ระบบ i18n ที่บังคับด้วย TypeScript"
|
||||
i18n_source_hash: 55837d9fdaef
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 2c7c9a24cb4d
|
||||
---
|
||||
|
||||
# คู่มือการแปล {#translation-guide}
|
||||
|
||||
SnapOtter มาพร้อมกับ 21 ภาษาตั้งแต่ต้น ระบบ i18n ใช้รันไทม์แบบกำหนดเองที่มีขนาดเบา พร้อมการบังคับความสมบูรณ์ของ locale ด้วย 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. **การตรวจจับอัตโนมัติของเบราว์เซอร์** - ไล่ไปตามอาร์เรย์ `navigator.languages` ด้วยการจับคู่คำนำหน้าแบบ BCP 47
|
||||
3. **ค่าเริ่มต้นของอินสแตนซ์** - ตัวแปรสภาพแวดล้อม `DEFAULT_LOCALE` ของผู้ดูแลระบบ (ดึงมาจาก `GET /api/v1/config/locale`)
|
||||
4. **การถอยกลับเป็นภาษาอังกฤษ** - พร้อมใช้งานเสมอ
|
||||
|
||||
ผู้ใช้สามารถเปลี่ยนภาษาได้จาก:
|
||||
- **ตัวเลือกลูกโลกที่ส่วนท้าย** (เดสก์ท็อป มองเห็นได้เสมอ)
|
||||
- ตัวเลือกภาษาที่ **หน้าเข้าสู่ระบบ** (ก่อนการยืนยันตัวตน)
|
||||
- ส่วน **Settings > General** (การตั้งค่าเฉพาะผู้ใช้)
|
||||
- เมนูดรอปดาวน์ภาษาใน **แถบข้างบนมือถือ**
|
||||
- ส่วน **Settings > System** ตั้งค่าเริ่มต้นทั่วทั้งอินสแตนซ์ (เฉพาะผู้ดูแลระบบ)
|
||||
|
||||
## การแปลทำงานอย่างไร {#how-translations-work}
|
||||
|
||||
สตริง UI ทั้งหมดอยู่ใน `packages/shared/src/i18n/` ไฟล์อ้างอิงคือ `en.ts` ซึ่งส่งออกอ็อบเจกต์ที่มีการกำหนดชนิดข้อมูล พร้อมทุกสตริงที่แอปใช้ (~1500 คีย์) ภาษาอื่นๆ เป็นไฟล์แยกต่างหาก (เช่น `de.ts`, `fr.ts`) ที่ส่งออกในรูปแบบเดียวกัน
|
||||
|
||||
ชนิด `TranslationKeys` ใช้ `DeepStringRecord` เพื่อยอมรับค่าสตริงใดๆ ในขณะที่บังคับโครงสร้างคีย์ TypeScript จะตรวจจับคีย์ที่ขาดหายไปในไฟล์แปลใดๆ ณ เวลาคอมไพล์
|
||||
|
||||
จะโหลดเฉพาะ locale ที่ใช้งานอยู่เท่านั้น ณ รันไทม์ผ่าน `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 การแปลโดยตรง คุณสามารถปรับปรุง locale ที่มีอยู่หรือเพิ่ม locale ใหม่ได้
|
||||
|
||||
หากต้องการรายงานคำแปลที่ผิดโดยไม่ส่งโค้ด ให้เปิด [GitHub Issue](https://github.com/snapotter-hq/SnapOtter/issues) พร้อมระบุภาษา สตริงที่ไม่ถูกต้อง และการแก้ไขที่แนะนำ
|
||||
|
||||
::: tip
|
||||
PR การแปลไม่จำเป็นต้องได้รับการอนุมัติล่วงหน้า ให้ fork repo ทำการเปลี่ยนแปลงของคุณ แล้วเปิด PR ดู [Contributing Guide](/th/guide/contributing) สำหรับกระบวนการ PR ทั้งหมดและข้อกำหนด CLA
|
||||
:::
|
||||
|
||||
## วิธีสร้างหรืออัปเดตคำแปล {#how-to-create-or-update-a-translation}
|
||||
|
||||
### 1. Fork และ clone {#_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` ไว้ที่ท้าย
|
||||
- นำเข้า `TranslationKeys` จาก `./en.js` และกำหนดชนิดข้อมูลให้กับ export ของคุณ
|
||||
- คงตัวแทน `{variable}` ไว้ตามเดิมทุกประการ
|
||||
- อาร์เรย์ (`rotatingPhrases`, `progressMessages`) ต้องมีจำนวนรายการเท่ากัน
|
||||
- อย่าแปล: SnapOtter, JPEG, PNG, WebP, EXIF, API และคำศัพท์ทางเทคนิคอื่นๆ
|
||||
|
||||
### 4. ลงทะเบียน locale (สำหรับภาษาใหม่เท่านั้น) {#_4-register-the-locale-new-language-only}
|
||||
|
||||
เพิ่ม locale ของคุณลงใน `SUPPORTED_LOCALES` ใน `packages/shared/src/i18n/index.ts`:
|
||||
|
||||
```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}
|
||||
|
||||
เปิด PR ต่อ `main` ด้วยชื่อเรื่องอย่างเช่น `feat(i18n): add Swedish translation` หรือ `fix(i18n): correct German typos` บอต CLA จะขอให้คุณลงนามในการมีส่วนร่วมครั้งแรกของคุณ
|
||||
|
||||
## การเพิ่มคีย์คำแปลใหม่ {#adding-new-translation-keys}
|
||||
|
||||
เมื่อเพิ่มฟีเจอร์ใหม่ที่ต้องการสตริง UI ใหม่:
|
||||
|
||||
1. เพิ่มคีย์ใหม่ลงใน `en.ts` ก่อน (ไฟล์อ้างอิง)
|
||||
2. รัน `pnpm typecheck` - ไฟล์ locale ทุกไฟล์จะล้มเหลวหากขาดคีย์ใหม่
|
||||
3. เพิ่มคีย์ใหม่ลงในไฟล์ locale ทั้งหมด (ใช้ภาษาอังกฤษเป็นค่าถอยกลับชั่วคราว)
|
||||
|
||||
## การกำหนดค่า {#configuration}
|
||||
|
||||
ตั้งค่าภาษาเริ่มต้นของอินสแตนซ์ผ่านตัวแปรสภาพแวดล้อม:
|
||||
|
||||
```yaml
|
||||
DEFAULT_LOCALE: "de" # German as the default for all new users
|
||||
```
|
||||
|
||||
## การอ้างอิงไฟล์ {#file-reference}
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `packages/shared/src/i18n/en.ts` | สตริงภาษาอังกฤษ (locale อ้างอิง, ~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()` |
|
||||
| `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}
|
||||
|
||||
แต่ละหน่วยของต้นฉบับภาษาอังกฤษที่แปลได้จะถูกแฮช และแฮชจะถูกจัดเก็บ
|
||||
ไว้ข้างคำแปลของมัน ในการรันแต่ละครั้ง ไปป์ไลน์จะ:
|
||||
|
||||
- แปลหน่วยใดๆ ที่ยังไม่มีคำแปล
|
||||
- ข้ามหน่วยใดๆ ที่แฮชที่จัดเก็บไว้ยังตรงกับต้นฉบับภาษาอังกฤษ
|
||||
- แปลใหม่สำหรับหน่วยที่เป็น **machine** เมื่อต้นฉบับภาษาอังกฤษเปลี่ยนแปลง
|
||||
- และตั้งค่าสถานะหน่วยที่ผ่านการปรับปรุงโดย **human** เป็น `stale` (ต้องตรวจทาน) เมื่อต้นฉบับ
|
||||
ภาษาอังกฤษเปลี่ยนแปลง แทนที่จะเขียนทับงานของคุณ
|
||||
|
||||
### การปรับปรุงคำแปลเว็บด้วย PR {#refining-a-web-translation-by-pr}
|
||||
|
||||
คุณปรับปรุงคำแปลของเว็บไซต์ เอกสาร หรือการอ้างอิง API ด้วยวิธีเดียวกับที่คุณ
|
||||
ปรับปรุง locale ของแอป: โดยแก้ไขไฟล์ที่ถูกสร้างขึ้นและเปิด 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 key เพื่อ
|
||||
มีส่วนร่วม เพียงแก้ไขไฟล์ที่ถูกสร้างขึ้นและเปิด PR ดู
|
||||
[`scripts/i18n/README.md`](https://github.com/snapotter-hq/SnapOtter/blob/main/scripts/i18n/README.md)
|
||||
สำหรับวิธีการทำงานของไปป์ไลน์
|
||||
:::
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
i18n_source_hash: 9a6abf3fc8ae
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: a1ade9bc05db
|
||||
---
|
||||
# การอัปเกรดจาก 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 key และไปป์ไลน์ของคุณจะถูกย้ายมาด้วย ฐานข้อมูลเดิมจะไม่ถูกแก้ไข ดังนั้นคุณสามารถย้อนกลับได้เสมอ
|
||||
|
||||
::: tip หมายเหตุถึงผู้ใช้ 1.x ของเรา
|
||||
พวกคุณหลายคนไว้วางใจ SnapOtter มาตั้งแต่วันแรก และคำติชมของคุณได้หล่อหลอมรีลีสนี้ 2.0 เปลี่ยนแปลงหลายอย่างเบื้องหลัง และคู่มือนี้มีอยู่เพื่อให้การย้ายไม่ทำให้คุณต้องเสียสิ่งที่คุณใส่ใจ บัญชี ไฟล์ การตั้งค่า API key และไปป์ไลน์ของคุณจะถูกย้ายมา และฐานข้อมูลเดิมของคุณจะไม่ถูกแตะต้อง ขอบคุณที่อัปเกรดไปพร้อมกับเรา
|
||||
:::
|
||||
|
||||
## ก่อนเริ่ม: สำรองข้อมูลโวลุ่ม `/data` ทั้งหมด {#before-you-start-back-up-the-whole-data-volume}
|
||||
|
||||
ทำสิ่งนี้ก่อนเสมอ ทุกครั้ง สำรองข้อมูลโวลุ่ม `/data` **ทั้งหมด** ไม่ใช่แค่ไฟล์ `snapotter.db`
|
||||
|
||||
นี่คือเหตุผลว่าทำไมมันจึงสำคัญ 1.x รัน SQLite ในโหมด WAL ดังนั้นคอนเทนเนอร์ 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}
|
||||
|
||||
อัปเกรดการติดตั้ง 1.x ของคุณให้เป็นรีลีส 1.x ล่าสุด (1.17.2) ก่อนย้ายไปยัง 2.0 นั่นช่วยให้ 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}
|
||||
|
||||
หากคุณรัน SnapOtter ด้วย `docker run` เดียว ก็ทำแบบนั้นต่อไป 2.0 จะบูต PostgreSQL และ Redis แบบฝังตัวภายในคอนเทนเนอร์เมื่อคุณไม่ได้ตั้งค่า `DATABASE_URL` หรือ `REDIS_URL` และตรวจจับพร้อมนำเข้า `/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}
|
||||
|
||||
สแตก Compose ของ 2.0 รันสามบริการ (แอป, Postgres, Redis) ใช้โวลุ่ม `/data` ของ 1.x ซ้ำสำหรับบริการแอป แอปจะตรวจจับ `/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 key (ยังใช้งานได้ต่อไป) และไปป์ไลน์ที่บันทึกไว้
|
||||
- ระเบียนประวัติงาน
|
||||
- ไลบรารีไฟล์ที่บันทึกไว้ของคุณ ทั้งระเบียนและตัวไฟล์จริง เพราะ `/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}
|
||||
|
||||
การอัปเกรดจะไม่แก้ไขหรือลบ `snapotter.db` 1.x ของคุณ หากคุณต้องการกลับไปใช้ 1.x ให้ปรับใช้อิมเมจ 1.x อีกครั้งกับโวลุ่มเดียวกัน สิ่งที่คุณสร้างใน 2.0 หลังการอัปเกรดอยู่ใน Postgres และจะไม่อยู่ในฐานข้อมูล 1.x ดังนั้นให้ย้อนกลับโดยเร็วหากคุณจะทำ
|
||||
@@ -0,0 +1,264 @@
|
||||
---
|
||||
description: "จัดการผู้ใช้ บทบาทในตัวและบทบาทกำหนดเอง สิทธิ์ API key ทีม เซสชัน และบันทึกการตรวจสอบใน SnapOtter"
|
||||
i18n_source_hash: 5e28af686c96
|
||||
i18n_provenance: human
|
||||
i18n_output_hash: 28b3c25775a5
|
||||
---
|
||||
|
||||
# ผู้ใช้ บทบาท และสิทธิ์ {#users-roles-permissions}
|
||||
|
||||
SnapOtter มาพร้อมบทบาทในตัวสามบทบาท สิทธิ์แบบละเอียด 17 รายการ และรองรับบทบาทกำหนดเองพร้อมการควบคุมการเข้าถึงต่อเครื่องมือแบบไม่บังคับ หน้านี้ครอบคลุมโมเดลการให้สิทธิ์แบบเต็ม การกำหนดขอบเขต API key การจัดการทีม และการบันทึกการตรวจสอบ
|
||||
|
||||
::: tip หน้าที่เกี่ยวข้อง
|
||||
[OIDC / SSO](/th/guide/oidc) | [SAML SSO](/th/guide/saml) | [SCIM Provisioning](/th/guide/scim) | [Security & Hardening](/th/guide/security)
|
||||
:::
|
||||
|
||||
## ผู้ใช้ {#users}
|
||||
|
||||
### การสร้างผู้ใช้ {#creating-users}
|
||||
|
||||
ผู้ดูแลระบบสามารถสร้างผู้ใช้ผ่านแผงควบคุมผู้ดูแลระบบหรือเอ็นด์พอยต์ `POST /api/auth/register` ผู้ใช้แต่ละคนมีชื่อผู้ใช้ บทบาท การกำหนดทีม และที่อยู่อีเมลแบบไม่บังคับ
|
||||
|
||||
### ผู้ดูแลระบบเริ่มต้น {#default-admin}
|
||||
|
||||
ในการเริ่มต้นครั้งแรก SnapOtter จะสร้างบัญชีผู้ดูแลระบบเริ่มต้น ข้อมูลรับรองมาจากตัวแปรสภาพแวดล้อม:
|
||||
|
||||
| ตัวแปร | ค่าเริ่มต้น | คำอธิบาย |
|
||||
|---|---|---|
|
||||
| `DEFAULT_USERNAME` | `admin` | ชื่อผู้ใช้สำหรับบัญชีผู้ดูแลระบบเริ่มแรก |
|
||||
| `DEFAULT_PASSWORD` | `admin` | รหัสผ่านสำหรับบัญชีผู้ดูแลระบบเริ่มแรก |
|
||||
|
||||
ผู้ดูแลระบบเริ่มต้นจำเป็นต้องเปลี่ยนรหัสผ่านในการเข้าสู่ระบบครั้งแรก
|
||||
|
||||
### ผู้ให้บริการยืนยันตัวตน {#authentication-providers}
|
||||
|
||||
ผู้ใช้สามารถยืนยันตัวตนได้ผ่านหลายวิธี:
|
||||
|
||||
- **Local** - ชื่อผู้ใช้และรหัสผ่านที่จัดเก็บในฐานข้อมูล SnapOtter
|
||||
- **OIDC** - ผู้ให้บริการ OpenID Connect ใด ๆ (ดู [OIDC / SSO](/th/guide/oidc))
|
||||
- **SAML** - ผู้ให้บริการข้อมูลระบุตัวตน SAML 2.0 (ดู [SAML SSO](/th/guide/saml))
|
||||
- **SCIM** - การจัดสรรอัตโนมัติจากผู้ให้บริการข้อมูลระบุตัวตน (ดู [SCIM Provisioning](/th/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}
|
||||
|
||||
| สิทธิ์ | คำอธิบาย |
|
||||
|---|---|
|
||||
| `tools:use` | ใช้เครื่องมือประมวลผลใด ๆ |
|
||||
| `files:own` | ดูและจัดการไฟล์ของตนเอง |
|
||||
| `files:all` | ดูและจัดการไฟล์ของผู้ใช้ทั้งหมด |
|
||||
| `apikeys:own` | สร้างและจัดการ API key ของตนเอง |
|
||||
| `apikeys:all` | ดู API key ของผู้ใช้ทั้งหมด |
|
||||
| `pipelines:own` | สร้างและจัดการไปป์ไลน์ของตนเอง |
|
||||
| `pipelines:all` | ดูและจัดการไปป์ไลน์ของผู้ใช้ทั้งหมด |
|
||||
| `settings:read` | ดูการตั้งค่าอินสแตนซ์ |
|
||||
| `settings:write` | แก้ไขการตั้งค่าอินสแตนซ์ |
|
||||
| `users:manage` | สร้าง อัปเดต และลบบัญชีผู้ใช้ |
|
||||
| `teams:manage` | สร้าง อัปเดต และลบทีม |
|
||||
| `features:manage` | ติดตั้งและจัดการบันเดิลฟีเจอร์ AI |
|
||||
| `system:health` | เข้าถึงเอ็นด์พอยต์ health และ readiness |
|
||||
| `audit:read` | ดูบันทึกการตรวจสอบและแสดงรายการบทบาท |
|
||||
| `compliance:manage` | จัดการวงจร GDPR และฟีเจอร์การปฏิบัติตามข้อกำหนด |
|
||||
| `webhooks:manage` | กำหนดค่า webhook ขาออก |
|
||||
| `security:manage` | จัดการการตั้งค่าความปลอดภัย (IP allowlist, การบังคับใช้ SSO) |
|
||||
|
||||
## บทบาทกำหนดเอง {#custom-roles}
|
||||
|
||||
ผู้ดูแลระบบที่มีสิทธิ์ `security:manage` สามารถสร้างบทบาทกำหนดเองผ่านแผงควบคุมผู้ดูแลระบบหรือ roles 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`
|
||||
|
||||
roles API จะปฏิเสธคำขอใด ๆ ที่มีสิทธิ์เหล่านี้ มีเพียงบทบาท `admin` ในตัวเท่านั้นที่มีสิทธิ์เข้าถึง
|
||||
|
||||
### สิทธิ์ระดับเครื่องมือ {#tool-level-permissions}
|
||||
|
||||
บทบาทกำหนดเองสามารถจำกัดว่าผู้ใช้เข้าถึงเครื่องมือใดได้บ้างแบบไม่บังคับ มีสองโหมดให้ใช้:
|
||||
|
||||
| โหมด | พฤติกรรม | ข้อกำหนดใบอนุญาต |
|
||||
|---|---|---|
|
||||
| `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` จะถูกสร้างในการเริ่มต้นครั้งแรก
|
||||
|
||||
| ฟิลด์ | ชนิด | คำอธิบาย |
|
||||
|---|---|---|
|
||||
| `name` | string | ชื่อทีมที่ไม่ซ้ำกัน (1-50 อักขระ) |
|
||||
| `storageQuota` | number | ขีดจำกัดพื้นที่จัดเก็บต่อทีมเป็นไบต์ (ใช้งานได้โดยไม่ต้องมีเอ็นเทอร์ไพรส์) |
|
||||
| `retentionHours` | number | ลบเอาต์พุตอัตโนมัติหลังจากผ่านไปกี่ชั่วโมง (ต้องใช้ `team_retention_overrides`, เอ็นเทอร์ไพรส์) |
|
||||
| `legalHold` | boolean | ป้องกันการลบไฟล์ของสมาชิกทีมโดยอัตโนมัติ (ต้องใช้ `legal_hold`, เอ็นเทอร์ไพรส์) |
|
||||
|
||||
::: info
|
||||
ทีม `Default` ไม่สามารถลบได้ ทีมที่ยังมีสมาชิกอยู่ก็ไม่สามารถลบได้ ให้กำหนดสมาชิกใหม่ก่อน
|
||||
:::
|
||||
|
||||
## API key {#api-keys}
|
||||
|
||||
ผู้ใช้สามารถสร้าง API key สำหรับการเข้าถึงแบบโปรแกรม แต่ละคีย์ใช้คำนำหน้า `si_` และแสดงเพียงครั้งเดียวในตอนสร้าง
|
||||
|
||||
### สิทธิ์แบบกำหนดขอบเขต {#scoped-permissions}
|
||||
|
||||
API key สามารถมีอาร์เรย์ `permissions` แบบไม่บังคับได้ เมื่อตั้งค่าแล้ว สิทธิ์ที่มีผลสำหรับคำขอจะเป็น **การตัดกัน** ของสิทธิ์ตามบทบาทของผู้ใช้และสิทธิ์แบบกำหนดขอบเขตของคีย์ นี่หมายความว่า API key ไม่สามารถยกระดับเกินสิทธิ์ของตัวผู้ใช้เองได้
|
||||
|
||||
```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}
|
||||
|
||||
คีย์รับ timestamp `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` (มีให้ในทั้งแผนทีมและเอ็นเทอร์ไพรส์)
|
||||
|
||||
หากไม่มีสิ่งใดสิ่งหนึ่งเหล่านี้ การดำเนินการเครื่องมือแต่ละครั้งจะไม่ถูกบันทึกในบันทึกการตรวจสอบ
|
||||
:::
|
||||
|
||||
### การเอ็กซ์พอร์ต {#exporting}
|
||||
|
||||
```
|
||||
GET /api/v1/enterprise/audit/export?format=csv&from=2026-01-01T00:00:00Z
|
||||
```
|
||||
|
||||
ต้องใช้สิทธิ์ `audit:read` และฟีเจอร์เอ็นเทอร์ไพรส์ `audit_export` (มีให้ในทั้งแผนทีมและเอ็นเทอร์ไพรส์) รองรับรูปแบบ CSV และ JSON กรองตาม `action`, `actorId`, `targetType`, `targetId`, `from` และ `to`
|
||||
|
||||
### การลงนามแบบทนต่อการดัดแปลง {#tamper-resistant-signing}
|
||||
|
||||
เมื่อเปิดใช้งาน แต่ละรายการในบันทึกการตรวจสอบจะถูกลงนามด้วย HMAC ที่ได้มาจาก `DATA_ENCRYPTION_KEY` สิ่งนี้ต้องการ:
|
||||
|
||||
1. การตั้งค่า `DATA_ENCRYPTION_KEY` ในสภาพแวดล้อมของคุณ
|
||||
2. การเปิดใช้งานการตั้งค่าผู้ดูแลระบบ `tamperResistantAudit`
|
||||
3. ใบอนุญาตเอ็นเทอร์ไพรส์พร้อมฟีเจอร์ `tamper_resistant_audit`
|
||||
|
||||
### การเก็บรักษา {#retention}
|
||||
|
||||
ตั้งค่า `AUDIT_RETENTION_DAYS` เพื่อล้างรายการเก่าโดยอัตโนมัติ ค่าเริ่มต้นคือ `0` ซึ่งหมายความว่ารายการจะถูกเก็บไว้อย่างไม่มีกำหนด
|
||||
|
||||
### การอ้างอิงเหตุการณ์ {#event-reference}
|
||||
|
||||
| เหตุการณ์ | หมวดหมู่ |
|
||||
|---|---|
|
||||
| `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 (เลือกเข้าร่วม) |
|
||||
| `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}
|
||||
|
||||
เซสชันใช้คุกกี้ ควบคุมโดย `SESSION_DURATION_HOURS` (ค่าเริ่มต้น: 168 ชั่วโมง / 7 วัน)
|
||||
|
||||
### การเปลี่ยนบทบาททำให้เซสชันเป็นโมฆะ {#role-changes-invalidate-sessions}
|
||||
|
||||
เมื่อผู้ดูแลระบบเปลี่ยนบทบาทของผู้ใช้ เซสชันที่ใช้งานอยู่ทั้งหมดของผู้ใช้นั้นจะถูกลบ ผู้ใช้ต้องเข้าสู่ระบบอีกครั้งเพื่อรับสิทธิ์ใหม่
|
||||
|
||||
### การป้องกันเพื่อความปลอดภัย {#safety-guards}
|
||||
|
||||
- **การป้องกันผู้ดูแลระบบคนสุดท้าย**: ผู้ดูแลระบบคนสุดท้ายที่เหลืออยู่ไม่สามารถถูกลดระดับไปยังบทบาทที่ต่ำกว่าได้ API จะคืนค่าข้อผิดพลาดหากคุณพยายามทำ
|
||||
- **การป้องกันการลบตนเอง**: ผู้ดูแลระบบไม่สามารถลบบัญชีของตนเองผ่าน API ได้
|
||||
Reference in New Issue
Block a user