From 76a15ecabbf6a4dfa79d1ab9cc2598609f5b66f7 Mon Sep 17 00:00:00 2001 From: ChenKaiLiuG Date: Sat, 20 Dec 2025 23:25:42 +0800 Subject: [PATCH] Rebuild --- QUICKSTART.md | 122 --------- README.md | 374 ++++++++++++++++++++------- {human-gpt => api}/requirements.txt | 2 + api/server.py | 207 +++++++++++++++ docker-compose.yml | 90 +++++++ docker-stack/.env.example | 34 --- docker-stack/docker-compose.yml | 72 ------ human-gpt/.dockerignore | 17 -- human-gpt/Dockerfile | 18 -- human-gpt/human_reply_server.py | 384 ---------------------------- proxy-docker-compose.yml | 44 ++++ 11 files changed, 620 insertions(+), 744 deletions(-) delete mode 100644 QUICKSTART.md rename {human-gpt => api}/requirements.txt (55%) create mode 100644 api/server.py create mode 100644 docker-compose.yml delete mode 100644 docker-stack/.env.example delete mode 100644 docker-stack/docker-compose.yml delete mode 100644 human-gpt/.dockerignore delete mode 100644 human-gpt/Dockerfile delete mode 100644 human-gpt/human_reply_server.py create mode 100644 proxy-docker-compose.yml diff --git a/QUICKSTART.md b/QUICKSTART.md deleted file mode 100644 index 0742bba..0000000 --- a/QUICKSTART.md +++ /dev/null @@ -1,122 +0,0 @@ -# TobiichiGPT - 快速設定指南 - -## 📦 一鍵啟動 - -```powershell -# 進入 docker-stack 目錄 -cd docker-stack - -# 啟動所有服務 -docker-compose up -d -``` - -## 🌐 服務清單 - -| 服務 | 網址 | 用途 | -|------|------|------| -| Open WebUI | http://localhost:3000 | 用戶對話介面 | -| 管理員後台 | http://localhost:8000/admin | 真人回覆訊息 | -| NPM 管理面板 | http://localhost:82 | 反向代理設定 | -| Cloudflare Tunnel | 自動連線 | 公開訪問(需設定) | - -## ⚙️ Open WebUI 設定步驟 - -1. 開啟 http://localhost:3000 -2. 進入 **Settings** → **Connections** -3. 點擊 **+ Add OpenAI Connection** -4. 填入: - - **API Base URL**: `http://human-reply-server:8000/v1` - - **API Key**: `sk-human` (隨便填) -5. 模型列表會出現 **human-admin** - -## 💬 使用流程 - -### 用戶端 -1. 在 Open WebUI 選擇 `human-admin` 模型 -2. 輸入訊息後送出 -3. 等待管理員回覆(畫面會轉圈) - -### 管理員端 -1. 訪問 http://localhost:8000/admin -2. 看到新訊息通知 -3. 在文字框輸入回覆 -4. 點擊「送出回覆」 - -## 🔒 設定 HTTPS - -### 方法 1: 使用 NPM(本機 NPM) - -1. 訪問 http://localhost:82 -2. 登入(預設帳密:admin@example.com / changeme) -3. 首次登入會要求更改密碼 -4. 新增 Proxy Host: - - **Domain Names**: 你的網域 (例如: chat.example.com) - - **Forward Hostname**: `open-webui` - - **Forward Port**: `8080` - - 勾選 **SSL** → 申請 Let's Encrypt 憑證 - -### 方法 2: 使用 Cloudflare Tunnel(推薦) - -1. 前往 https://one.dash.cloudflare.com/ -2. 建立 Tunnel 並複製 Token -3. 編輯 `docker-stack/.env`,填入 Token -4. 在 Cloudflare 設定 Public Hostname: - - `chat.yourdomain.com` → `open-webui:8080` - - `admin.yourdomain.com` → `human-reply-server:8000` -5. 自動獲得 HTTPS + DDoS 保護 - -## 🛠️ 常用指令 - -```powershell -# 查看服務狀態 -docker-compose ps - -# 查看日誌 -docker-compose logs -f - -# 重啟服務 -docker-compose restart - -# 停止服務 -docker-compose down - -# 完全移除(包含資料) -docker-compose down -v -``` - -## 🐛 疑難排解 - -### Open WebUI 連不到人工回覆伺服器 -- 確認 API URL 使用 `http://human-reply-server:8000/v1`(容器名稱) -- 不要使用 `localhost:8000` - -### 管理員後台打不開 -- 確認容器是否正常運行:`docker ps` -- 查看錯誤日誌:`docker logs tobiichi-gpt` - -### NPM 無法訪問 -- 確認 port 81 沒有被佔用 -- Windows 防火牆可能需要開放 port - -## 📝 Port 對應 - -### 本機訪問 - -| 容器內 Port | 本機 Port | 服務 | -|------------|----------|------| -| 8080 | 3000 | Open WebUI | -| 8000 | 8000 | 人工回覆 API | -| 80 | 8080 | 本專案 NPM HTTP | -| 443 | 8443 | 本專案 NPM HTTPS | -| 81 | 82 | 本專案 NPM 管理 | - -### 與現有 NPM 共存 - -| 服務 | Port | 說明 | -|------|------|------| -| 現有 NPM | 80/443/81 | 原有的 NPM 實例 | -| 本專案 NPM | 8080/8443/82 | TobiichiGPT 專用 NPM | - -### 透過 Cloudflare Tunnel - -無需開放 Port,直接使用網域訪問(需設定 `.env` 檔案) diff --git a/README.md b/README.md index 17fcd6c..3a848bb 100644 --- a/README.md +++ b/README.md @@ -5,161 +5,341 @@ ## 🎯 特色 - ✅ **零修改** - 不需要修改 Open WebUI 程式碼 -- ✅ **極簡化** - 單一 Python 檔案即可運行 -- ✅ **即插即用** - 偽裝成 OpenAI API,直接在 Open WebUI 設定中使用 -- ✅ **視覺化後台** - 美觀的網頁介面供管理員回覆 +- ✅ **極簡化** - 4 個容器完成所有功能 +- ✅ **共享資料庫** - Open WebUI 和管理後台使用同一個 PostgreSQL +- ✅ **視覺化後台** - NocoDB 提供專業的資料庫管理介面 +- ✅ **可編輯代碼** - API 程式碼透過 bind mount 可直接修改 + +## 🏗️ 架構 + +### 主要服務(必須) + +``` +┌─────────────────┐ +│ PostgreSQL │ ← 共享資料庫 +└────────┬────────┘ + │ + ┌────┴─────┬─────────────┬─────────────┐ + │ │ │ │ +┌───▼────┐ ┌──▼─────┐ ┌────▼─────┐ ┌───▼────┐ +│ API │ │ Open │ │ NocoDB │ │ 用戶 │ +│ 中間層 │ │ WebUI │ │ 管理介面 │ │ 瀏覽器 │ +└────────┘ └────────┘ └──────────┘ └────────┘ +``` + +### 代理服務(選用) + +``` +┌───────────┐ +│ 用戶 │ +└─────┬─────┘ + │ 公網 +┌─────▼──────────────┐ +│ Cloudflare Tunnel │ +└─────┬──────────────┘ + │ 內網 +┌─────▼──────────────┐ +│ Nginx Proxy Mgr │ +└─────┬──────────────┘ + │ +┌─────▼──────────────┐ +│ 主要服務 (上方) │ +└────────────────────┘ +``` ## 🚀 快速開始 -### 方法 1: Docker (推薦) +### 1. 準備環境檔案 ```powershell -# 進入 docker-stack 目錄 -cd docker-stack - -# 設定環境變數(首次使用) +# 複製主服務環境變數 Copy-Item .env.example .env -notepad .env # 填入 Cloudflare Tunnel Token(若要使用) +notepad .env # 設定 DB_PASSWORD -# 使用 Docker Compose 啟動 +# 如需代理服務 +Copy-Item .env.proxy .env.proxy +notepad .env.proxy # 填入 CLOUDFLARE_TUNNEL_TOKEN +``` + +### 2. 啟動主要服務 + +```powershell +# 啟動 PostgreSQL + API + Open WebUI + NocoDB docker-compose up -d +# 查看狀態 +docker-compose ps + # 查看日誌 docker-compose logs -f - -# 停止服務 -docker-compose down ``` -### 方法 2: 直接執行 +### 3. 啟動代理服務(選用) ```powershell -# 1. 進入 docker 目錄 -cd docker +# 啟動 Cloudflare Tunnel + NPM +docker-compose -f docker-compose.proxy.yml --env-file .env.proxy up -d -# 2. 安裝依賴 -pip install -r requirements.txt - -# 3. 啟動伺服器 -python human_reply_server.py -``` - -啟動後會看到: -``` -🚀 人工回覆伺服器啟動中... -📌 管理員後台: http://localhost:8000/admin -📌 API 端點: http://localhost:8000/v1 +# 查看狀態 +docker-compose -f docker-compose.proxy.yml ps ``` ## 🌐 服務訪問 -啟動後可訪問以下服務: +### 主要服務 | 服務 | 網址 | 說明 | |------|------|------| | **Open WebUI** | http://localhost:3000 | 用戶對話介面 | -| **管理員後台** | http://localhost:8000/admin | 真人回覆訊息 | -| **NPM 管理面板** | http://localhost:82 | Nginx Proxy Manager | -| **Cloudflare Tunnel** | 自動連線 | 無需手動訪問 | +| **API 伺服器** | http://localhost:8000 | OpenAI API 相容端點 | +| **NocoDB** | http://localhost:8080 | 管理員回覆介面 | +| **PostgreSQL** | localhost:5432 | 資料庫 | -預設帳號密碼(NPM): +### 代理服務(如已啟動) + +| 服務 | 網址 | 說明 | +|------|------|------| +| **NPM 管理面板** | http://localhost:81 | Nginx Proxy Manager | +| **HTTP 代理** | Port 80 | HTTP 流量 | +| **HTTPS 代理** | Port 443 | HTTPS 流量 | + +**NPM 預設帳號**: - Email: `admin@example.com` - Password: `changeme` -### 在 Open WebUI 中設定 +## 📋 使用流程 + +### 設定 Open WebUI 1. 開啟 http://localhost:3000 2. 進入 **Settings** → **Connections** -3. 點擊 **+ Add OpenAI Connection** -4. 填入設定: - - **API Base URL**: `http://human-reply-server:8000/v1` - - **API Key**: 隨便填 (例如: `sk-human`) -5. 儲存後,模型列表會出現 **human-admin** +3. 新增 OpenAI Connection: + - **API Base URL**: `http://api:8000/v1` + - **API Key**: `sk-human` (任意值) +4. 模型列表會出現 **human-admin** -### 開始使用 +### 管理員回覆 -1. **用戶端**: 在 Open WebUI (http://localhost:3000) 選擇 `human-admin` 模型,發送訊息 -2. **管理員**: 訪問 http://localhost:8000/admin,查看並回覆訊息 -3. **自動刷新**: 後台每 5 秒自動刷新,顯示新訊息 +1. 訪問 NocoDB: http://localhost:8080 +2. 連接到 PostgreSQL: + - Host: `postgres` + - Port: `5432` + - Database: `tobiichi` + - Username: `tobiichi` + - Password: (你在 .env 設定的密碼) +3. 開啟 `reply_queue` 表格 +4. 查看 `status='pending'` 的訊息 +5. 填入 `admin_reply` 欄位 +6. 將 `status` 改為 `replied` +7. 用戶會在 3 秒內收到回覆 -## 📋 運作流程 +### 完整流程 ``` -用戶發送訊息 +用戶在 Open WebUI 發送訊息 ↓ -Open WebUI 調用 API (轉圈圈等待) +API 收到請求,寫入 reply_queue (status='pending') ↓ -訊息進入待處理隊列 +API 每 3 秒檢查該訊息的 status ↓ -管理員在後台看到訊息 +管理員在 NocoDB 看到訊息,填入回覆並改 status='replied' ↓ -管理員輸入並送出回覆 +API 讀取 admin_reply 欄位 ↓ -API 回傳給 Open WebUI +回傳給 Open WebUI ↓ 用戶收到回覆 ``` ## 🔧 技術架構 -- **FastAPI**: 輕量級 Python Web 框架 -- **AsyncIO**: 異步等待管理員回覆 -- **偽裝 OpenAI API**: - - `/v1/models` - 模型列表 - - `/v1/chat/completions` - 聊天完成端點 +### 主要服務 -## 🐳 Docker 部署 +- **PostgreSQL 15**: 共享關聯式資料庫 +- **FastAPI**: Python Web 框架,提供 OpenAI API 相容端點 +- **Open WebUI**: 對話前端介面 +- **NocoDB**: 視覺化資料庫管理工具 -### 檔案結構 -``` -tobiichiGPT/ -├── docker/ # Docker 相關檔案 -│ ├── human_reply_server.py # 主程式 -│ ├── requirements.txt # Python 依賴 -│ ├── Dockerfile # Docker 映像檔 -│ └── .dockerignore # Docker 忽略檔案 -├── docker-stack/ # Docker Compose 配置 -│ ├── docker-compose.yml # 服務編排檔案 -│ └── .env.example # 環境變數範例 -├── README.md # 專案說明 -├── QUICKSTART.md # 快速開始指南 -└── LICENSE # 授權檔案 -``` +### 代理服務 -### Docker 指令 +- **Nginx Proxy Manager**: 反向代理 + SSL 憑證管理 +- **Cloudflare Tunnel**: 安全的公網訪問通道 + +### API 端點 + +- `/v1/models` - 模型列表(回傳 human-admin) +- `/v1/chat/completions` - 聊天完成端點(OpenAI 相容) + +## 🌍 Cloudflare Tunnel 設定 + +### 1. 建立 Tunnel + +1. 登入 [Cloudflare Zero Trust](https://one.dash.cloudflare.com/) +2. 前往 **Networks** → **Tunnels** +3. 點擊 **Create a tunnel** +4. 選擇 **Cloudflared** +5. 輸入 Tunnel 名稱(例如: `tobiichi-tunnel`) +6. 複製顯示的 Token +7. 將 Token 貼到 `.env.proxy` 的 `CLOUDFLARE_TUNNEL_TOKEN` + +### 2. 設定 Public Hostname + +#### 選項 A: 透過 NPM 代理(推薦) + +在 Cloudflare Tunnel 設定: +- **Public hostname**: `chat.yourdomain.com` +- **Service Type**: HTTP +- **URL**: `http://npm:80` + +然後在 NPM (http://localhost:81) 設定: +- **Domain**: `chat.yourdomain.com` +- **Forward to**: `openwebui:3000` + +#### 選項 B: 直接指向服務 + +在 Cloudflare Tunnel 設定: +- **Public hostname**: `chat.yourdomain.com` +- **Service Type**: HTTP +- **URL**: `http://openwebui:3000` + +### 3. 啟動 Tunnel ```powershell -# 建立映像檔 -cd docker -docker build -t tobiichi-gpt . - -# 直接執行容器 -docker run -d -p 8000:8000 --name tobiichi-gpt tobiichi-gpt - -# 使用 Docker Compose(推薦) -cd .\docker-stack -docker-compose up -d - -# 查看容器狀態 -docker ps - -# 查看日誌 -docker logs -f tobiichi-gpt - -# 停止並移除 -docker-compose down +docker-compose -f docker-compose.proxy.yml --env-file .env.proxy up -d ``` -### 與 Open WebUI 整合 +訪問你設定的網域即可從公網訪問服務。 -如果 Open WebUI 也在 Docker 中運行,將兩者加入同一網路: +## 📦 資料持久化 + +所有服務資料都會持久化保存: ```yaml -# 在 docker-compose.yml 中加入 Open WebUI -version: '3.8' +volumes: + postgres-data: # PostgreSQL 資料 + openwebui-data: # Open WebUI 配置和對話記錄 + nocodb-data: # NocoDB 配置 + npm-data: # NPM 配置(代理服務) + npm-letsencrypt: # SSL 憑證(代理服務) +``` -services: - human-reply-server: +## 🛠️ 修改 API 程式碼 + +API 程式碼位於 `./api/` 目錄,透過 bind mount 掛載到容器: + +```powershell +# 編輯 API 程式碼 +notepad api\server.py + +# 重啟 API 服務套用修改 +docker-compose restart api + +# 查看 API 日誌 +docker-compose logs -f api +``` + +## 🐳 檔案結構 + +``` +tobiichiGPT/ +├── api/ # API 服務程式碼 +│ ├── server.py # FastAPI 主程式 +│ └── requirements.txt # Python 依賴 +├── docker-compose.yml # 主要服務編排 +├── docker-compose.proxy.yml # 代理服務編排 +├── .env.example # 主服務環境變數範例 +├── .env.proxy # 代理服務環境變數範例 +└── README.md # 本文件 +``` + +## 🔍 故障排除 + +### API 無法連接資料庫 + +```powershell +# 檢查資料庫是否啟動 +docker-compose ps postgres + +# 查看資料庫日誌 +docker-compose logs postgres + +# 檢查網路連接 +docker network inspect tobiichi-network +``` + +### Open WebUI 無法連接 API + +1. 確認 API URL 使用容器名稱: `http://api:8000/v1` +2. 檢查兩個容器是否在同一網路: + ```powershell + docker network inspect tobiichi-network + ``` + +### NocoDB 無法連接資料庫 + +1. 檢查 PostgreSQL 是否健康: + ```powershell + docker-compose ps + ``` +2. 確認資料庫密碼正確(`.env` 檔案) + +### 代理服務無法啟動 + +```powershell +# 檢查主網路是否已建立 +docker network ls | Select-String "tobiichi-network" + +# 先啟動主服務 +docker-compose up -d + +# 再啟動代理服務 +docker-compose -f docker-compose.proxy.yml up -d +``` + +## 📝 開發筆記 + +### 資料庫 Schema + +`reply_queue` 表格結構: + +```sql +CREATE TABLE reply_queue ( + id SERIAL PRIMARY KEY, + user_message TEXT NOT NULL, + admin_reply TEXT, + status VARCHAR(20) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + replied_at TIMESTAMP +); + +CREATE INDEX idx_status ON reply_queue(status); +CREATE INDEX idx_created_at ON reply_queue(created_at); +``` + +### 環境變數 + +**主服務** (`.env`): +- `DB_PASSWORD`: PostgreSQL 密碼 + +**代理服務** (`.env.proxy`): +- `CLOUDFLARE_TUNNEL_TOKEN`: Cloudflare Tunnel 認證 Token + +### Port 映射 + +**主服務**: +- 3000: Open WebUI +- 5432: PostgreSQL +- 8000: API +- 8080: NocoDB + +**代理服務**: +- 80: HTTP +- 443: HTTPS +- 81: NPM 管理介面 + +## 📄 授權 + +MIT License build: . container_name: tobiichi-gpt ports: diff --git a/human-gpt/requirements.txt b/api/requirements.txt similarity index 55% rename from human-gpt/requirements.txt rename to api/requirements.txt index 8e26b4d..c6f9c17 100644 --- a/human-gpt/requirements.txt +++ b/api/requirements.txt @@ -1,3 +1,5 @@ fastapi==0.104.1 uvicorn==0.24.0 pydantic==2.5.0 +psycopg2-binary==2.9.9 +asyncpg==0.29.0 diff --git a/api/server.py b/api/server.py new file mode 100644 index 0000000..c417f85 --- /dev/null +++ b/api/server.py @@ -0,0 +1,207 @@ +""" +API 轉接層 - 偽裝 OpenAI API,將請求轉為人工回覆隊列 +""" + +from fastapi import FastAPI +from pydantic import BaseModel +import asyncpg +import asyncio +import time +import uuid +import os +from typing import Optional +from datetime import datetime + +app = FastAPI() + +# 資料庫連接池 +db_pool = None + +# 資料庫設定 +DB_CONFIG = { + "host": os.getenv("DB_HOST", "postgres"), + "port": int(os.getenv("DB_PORT", 5432)), + "database": os.getenv("DB_NAME", "tobiichi"), + "user": os.getenv("DB_USER", "tobiichi"), + "password": os.getenv("DB_PASSWORD", "tobiichi_password") +} + + +class Message(BaseModel): + role: str + content: str + + +class ChatRequest(BaseModel): + model: str + messages: list[Message] + stream: Optional[bool] = False + + +async def init_db(): + """初始化資料庫連接池和表格""" + global db_pool + db_pool = await asyncpg.create_pool(**DB_CONFIG, min_size=2, max_size=10) + + # 建立對話隊列表格 + async with db_pool.acquire() as conn: + await conn.execute(""" + CREATE TABLE IF NOT EXISTS reply_queue ( + id SERIAL PRIMARY KEY, + conversation_id VARCHAR(50) UNIQUE NOT NULL, + user_message TEXT NOT NULL, + admin_reply TEXT, + status VARCHAR(20) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT NOW(), + replied_at TIMESTAMP + ) + """) + + # 建立索引 + await conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_status ON reply_queue(status); + CREATE INDEX IF NOT EXISTS idx_conversation_id ON reply_queue(conversation_id); + """) + + +@app.on_event("startup") +async def startup(): + await init_db() + print("✅ 資料庫連接成功") + + +@app.on_event("shutdown") +async def shutdown(): + if db_pool: + await db_pool.close() + + +@app.get("/") +async def root(): + return { + "status": "TobiichiGPT API Running", + "database": "Connected" if db_pool else "Disconnected" + } + + +@app.get("/v1/models") +async def list_models(): + """模擬 OpenAI 模型列表""" + return { + "object": "list", + "data": [{ + "id": "human-admin", + "object": "model", + "created": int(time.time()), + "owned_by": "tobiichi" + }] + } + + +@app.post("/v1/chat/completions") +async def chat_completions(request: ChatRequest): + """接收用戶訊息,等待管理員回覆""" + + # 生成對話 ID + conv_id = str(uuid.uuid4())[:12] + + # 提取用戶訊息 + user_message = "" + for msg in reversed(request.messages): + if msg.role == "user": + user_message = msg.content + break + + if not user_message: + return { + "id": f"chatcmpl-{conv_id}", + "object": "chat.completion", + "created": int(time.time()), + "model": request.model, + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "無法識別您的訊息,請重新輸入。" + }, + "finish_reason": "stop" + }] + } + + # 寫入資料庫 + async with db_pool.acquire() as conn: + await conn.execute(""" + INSERT INTO reply_queue (conversation_id, user_message, status) + VALUES ($1, $2, 'pending') + """, conv_id, user_message) + + print(f"[新對話 {conv_id}] {user_message[:50]}...") + + # 等待管理員回覆(最多 15 分鐘) + timeout = 900 + start_time = time.time() + reply_content = None + + while time.time() - start_time < timeout: + async with db_pool.acquire() as conn: + row = await conn.fetchrow(""" + SELECT admin_reply, status + FROM reply_queue + WHERE conversation_id = $1 + """, conv_id) + + if row and row['status'] == 'replied' and row['admin_reply']: + reply_content = row['admin_reply'] + print(f"[已回覆 {conv_id}] {reply_content[:50]}...") + break + + await asyncio.sleep(3) # 每 3 秒檢查一次 + + # 如果超時 + if not reply_content: + reply_content = "抱歉,管理員暫時無法回覆,請稍後再試。" + async with db_pool.acquire() as conn: + await conn.execute(""" + UPDATE reply_queue + SET status = 'timeout' + WHERE conversation_id = $1 + """, conv_id) + + return { + "id": f"chatcmpl-{conv_id}", + "object": "chat.completion", + "created": int(time.time()), + "model": request.model, + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": reply_content + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": len(user_message), + "completion_tokens": len(reply_content) if reply_content else 0, + "total_tokens": len(user_message) + (len(reply_content) if reply_content else 0) + } + } + + +@app.get("/health") +async def health(): + """健康檢查""" + db_ok = db_pool is not None + return {"status": "healthy" if db_ok else "unhealthy", "database": db_ok} + + +if __name__ == "__main__": + import uvicorn + print("=" * 60) + print("🚀 TobiichiGPT API 啟動中...") + print("=" * 60) + print(f"📊 資料庫: {DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}") + print(f"🌐 API 端點: http://0.0.0.0:8000/v1") + print("=" * 60) + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cba38d3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,90 @@ +version: '3.8' + +services: + # PostgreSQL - 共用資料庫 + postgres: + image: postgres:15-alpine + container_name: tobiichiGPT-postgres + restart: unless-stopped + environment: + POSTGRES_DB: tobiichiGPT + POSTGRES_USER: tobiichi3227 + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - tobiichiGPT-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U tobiichi3227"] + interval: 10s + timeout: 5s + retries: 5 + + # API 轉接層 - 偽裝 OpenAI API + api: + image: python:3.11-slim + container_name: tobiichiGPT-api + restart: unless-stopped + ports: + - "18000:8000" + environment: + - DB_HOST=postgres + - DB_PORT=5432 + - DB_NAME=tobiichiGPT + - DB_USER=tobiichi3227 + - DB_PASSWORD=${DB_PASSWORD} + volumes: + - ./api:/app + working_dir: /app + command: > + sh -c " + pip install --no-cache-dir -r requirements.txt && + python server.py + " + networks: + - tobiichiGPT-network + depends_on: + postgres: + condition: service_healthy + + # Open WebUI - 用戶對話介面 + openwebui: + image: ghcr.io/open-webui/open-webui:main + container_name: tobiichiGPT-ui + restart: unless-stopped + ports: + - "13000:3000" + environment: + - DATABASE_URL=postgresql://tobiichi3227:${DB_PASSWORD}@postgres:5432/tobiichiGPT + - WEBUI_AUTH=True + volumes: + - openwebui-data:/app/backend/data + networks: + - tobiichiGPT-network + depends_on: + postgres: + condition: service_healthy + + # NocoDB - 管理員回覆介面 + nocodb: + image: nocodb/nocodb:latest + container_name: tobiichiGPT-nocodb + restart: unless-stopped + ports: + - "18080:8080" + environment: + - NC_DB=pg://postgres:5432?u=tobiichi3227&p=${DB_PASSWORD}&d=tobiichiGPT + networks: + - tobiichiGPT-network + depends_on: + postgres: + condition: service_healthy + +networks: + tobiichiGPT-network: + driver: bridge + name: tobiichiGPT-network # 固定網路名稱,讓 proxy stack 可以連接 + +volumes: + postgres-data: + openwebui-data: diff --git a/docker-stack/.env.example b/docker-stack/.env.example deleted file mode 100644 index 83e0035..0000000 --- a/docker-stack/.env.example +++ /dev/null @@ -1,34 +0,0 @@ -# Gitea Repository URL (替換為您的 Gitea 伺服器) -# 格式: http://gitea.example.com/username/tobiichiGPT.git -GIT_REPO_URL=http://your-gitea-server/your-username/tobiichiGPT.git - -# Git Branch (預設使用 main) -GIT_BRANCH=main - -# Container Registry (可選 - 如果要使用預建映像) -# 例如: gitea.example.com:5000 -REGISTRY_URL= - -# ======================================== -# Gitea Repository 設定 -# ======================================== -# Gitea Repository URL (格式: http://gitea.example.com/username/tobiichiGPT.git) -# 如需認證可使用: http://username:token@gitea.example.com/username/tobiichiGPT.git -GIT_REPO_URL=http://your-gitea-server/your-username/tobiichiGPT.git - -# Git Branch (預設: main) -GIT_BRANCH=main - -# ======================================== -# Container Registry (可選) -# ======================================== -# 如果已預先建置映像,填入 Registry 前綴 (例如: gitea.example.com:5000/username/) -# 留空則每次從 Gitea 即時建置 -REGISTRY_URL= - -# ======================================== -# Cloudflare Tunnel (可選) -# ======================================== -# 請到 Cloudflare Zero Trust Dashboard 建立 Tunnel 並取得 Token -# https://one.dash.cloudflare.com/ -CLOUDFLARE_TUNNEL_TOKEN= diff --git a/docker-stack/docker-compose.yml b/docker-stack/docker-compose.yml deleted file mode 100644 index d12eb16..0000000 --- a/docker-stack/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.8' - -services: - # 人工回覆伺服器 (從 Gitea 建置) - human-reply-server: - image: ${REGISTRY_URL}tobiichi-gpt:latest - build: - context: https://git.karylab.com/ChenKaiLiuG/tobiichiGPT.git#${GIT_BRANCH:-main} - dockerfile: human-gpt/Dockerfile - container_name: tobiichi-gpt - ports: - - "11000:8000" - restart: unless-stopped - environment: - - TZ=Asia/Taipei - networks: - - tobiichi-network - - # Open WebUI - open-webui: - image: ghcr.io/open-webui/open-webui:main - container_name: open-webui - ports: - - "10000:8080" - volumes: - - open-webui-data:/app/backend/data - environment: - - WEBUI_AUTH=False # 關閉登入驗證(可選) - - TZ=Asia/Taipei - restart: unless-stopped - networks: - - tobiichi-network - - # Nginx Proxy Manager - nginx-proxy-manager: - image: jc21/nginx-proxy-manager:latest - container_name: nginx-proxy-manager - ports: - - "10080:80" # HTTP (避免與現有 NPM 衝突) - - "10443:443" # HTTPS (避免與現有 NPM 衝突) - - "10081:81" # NPM 管理介面 (避免與現有 NPM 衝突) - volumes: - - npm-data:/data - - npm-letsencrypt:/etc/letsencrypt - environment: - - TZ=Asia/Taipei - restart: unless-stopped - networks: - - tobiichi-network - - # Cloudflare Tunnel - cloudflared: - image: cloudflare/cloudflared:latest - container_name: cloudflared-tunnel - command: tunnel run - environment: - - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} - restart: unless-stopped - networks: - - tobiichi-network - depends_on: - - open-webui - - human-reply-server - -networks: - tobiichi-network: - driver: bridge - -volumes: - open-webui-data: - npm-data: - npm-letsencrypt: diff --git a/human-gpt/.dockerignore b/human-gpt/.dockerignore deleted file mode 100644 index f7aa7bd..0000000 --- a/human-gpt/.dockerignore +++ /dev/null @@ -1,17 +0,0 @@ -__pycache__ -*.pyc -*.pyo -*.pyd -.Python -*.so -*.egg -*.egg-info -dist -build -.git -.gitignore -*.md -LICENSE -.vscode -.idea -*.log diff --git a/human-gpt/Dockerfile b/human-gpt/Dockerfile deleted file mode 100644 index 41c1707..0000000 --- a/human-gpt/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -# 複製依賴檔案 -COPY requirements.txt . - -# 安裝依賴 -RUN pip install --no-cache-dir -r requirements.txt - -# 複製程式檔案 -COPY human_reply_server.py . - -# 暴露端口 -EXPOSE 8000 - -# 啟動伺服器 -CMD ["python", "human_reply_server.py"] diff --git a/human-gpt/human_reply_server.py b/human-gpt/human_reply_server.py deleted file mode 100644 index 1d0609c..0000000 --- a/human-gpt/human_reply_server.py +++ /dev/null @@ -1,384 +0,0 @@ -""" -人工回覆伺服器 - 偽裝成 OpenAI API -使用方式: -1. 執行此程式:python human_reply_server.py -2. 在 Open WebUI 設定中加入模型:http://localhost:8000/v1 -3. 用戶發送訊息後,訪問 http://localhost:8000/admin 查看並回覆 -""" - -from fastapi import FastAPI, Request -from fastapi.responses import HTMLResponse, StreamingResponse -from pydantic import BaseModel -import asyncio -import json -import time -from datetime import datetime -from typing import Optional -import uuid - -app = FastAPI() - -# 儲存待處理的對話 -pending_conversations = {} -# 儲存管理員的回覆 -admin_replies = {} - - -class Message(BaseModel): - role: str - content: str - - -class ChatRequest(BaseModel): - model: str - messages: list[Message] - stream: Optional[bool] = False - - -@app.get("/") -async def root(): - return {"status": "Human Reply Server Running", "admin_panel": "/admin"} - - -@app.get("/v1/models") -async def list_models(): - """模擬 OpenAI 的模型列表 API""" - return { - "object": "list", - "data": [ - { - "id": "human-admin", - "object": "model", - "created": int(time.time()), - "owned_by": "human-admin" - } - ] - } - - -@app.post("/v1/chat/completions") -async def chat_completions(request: ChatRequest): - """模擬 OpenAI 的聊天完成 API - 等待真人回覆""" - - # 生成對話 ID - conversation_id = str(uuid.uuid4())[:8] - - # 取得最後一則用戶訊息 - user_message = "" - for msg in reversed(request.messages): - if msg.role == "user": - user_message = msg.content - break - - # 儲存對話資訊 - pending_conversations[conversation_id] = { - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "user_message": user_message, - "full_history": [{"role": msg.role, "content": msg.content} for msg in request.messages] - } - - print(f"\n[新訊息 {conversation_id}] {user_message}") - print(f"→ 等待管理員回覆,請訪問: http://localhost:8000/admin") - - # 等待管理員回覆 (最多等待 10 分鐘) - timeout = 600 - elapsed = 0 - while conversation_id not in admin_replies and elapsed < timeout: - await asyncio.sleep(1) - elapsed += 1 - - # 取得管理員回覆 - if conversation_id in admin_replies: - reply_content = admin_replies[conversation_id] - del admin_replies[conversation_id] - del pending_conversations[conversation_id] - else: - reply_content = "抱歉,管理員暫時無法回覆,請稍後再試。" - - # 根據是否需要串流回傳 - if request.stream: - return StreamingResponse( - stream_response(reply_content), - media_type="text/event-stream" - ) - else: - return { - "id": f"chatcmpl-{conversation_id}", - "object": "chat.completion", - "created": int(time.time()), - "model": request.model, - "choices": [{ - "index": 0, - "message": { - "role": "assistant", - "content": reply_content - }, - "finish_reason": "stop" - }], - "usage": { - "prompt_tokens": 0, - "completion_tokens": 0, - "total_tokens": 0 - } - } - - -async def stream_response(content: str): - """串流方式回傳回覆""" - # 開始標記 - yield f"data: {json.dumps({'choices': [{'delta': {'role': 'assistant'}, 'index': 0}]})}\n\n" - - # 逐字回傳 - for char in content: - chunk = { - "choices": [{ - "delta": {"content": char}, - "index": 0 - }] - } - yield f"data: {json.dumps(chunk)}\n\n" - await asyncio.sleep(0.01) - - # 結束標記 - yield f"data: {json.dumps({'choices': [{'delta': {}, 'index': 0, 'finish_reason': 'stop'}]})}\n\n" - yield "data: [DONE]\n\n" - - -@app.get("/admin", response_class=HTMLResponse) -async def admin_panel(): - """管理員後台 - 查看並回覆訊息""" - - # 生成待處理對話的 HTML - conversations_html = "" - if pending_conversations: - for conv_id, conv_data in pending_conversations.items(): - conversations_html += f""" -
-
- 對話 ID: {conv_id} - {conv_data['timestamp']} -
-
- 用戶: {conv_data['user_message']} -
- - -
- """ - else: - conversations_html = '

目前沒有待處理的訊息

' - - html = f""" - - - - - - 管理員後台 - 人工回覆系統 - - - -
-
-

🎯 管理員後台

-

人工回覆系統 - 待處理訊息數量: {len(pending_conversations)}

- -
- {conversations_html} -
-
- - - - - """ - return html - - -@app.post("/admin/reply") -async def admin_reply(request: Request): - """接收管理員的回覆""" - data = await request.json() - conversation_id = data.get("conversation_id") - reply = data.get("reply") - - if conversation_id and reply: - admin_replies[conversation_id] = reply - print(f"[管理員回覆 {conversation_id}] {reply}") - return {"status": "success"} - - return {"status": "error", "message": "Missing conversation_id or reply"} - - -if __name__ == "__main__": - import uvicorn - print("=" * 60) - print("🚀 人工回覆伺服器啟動中...") - print("=" * 60) - print("📌 管理員後台: http://localhost:8000/admin") - print("📌 API 端點: http://localhost:8000/v1") - print("=" * 60) - print("\n在 Open WebUI 中設定:") - print("1. 進入 Settings → Connections") - print("2. 添加 OpenAI API:") - print(" - API Base URL: http://localhost:8000/v1") - print(" - API Key: 隨便填 (例如: sk-human)") - print("3. 模型名稱會顯示為: human-admin") - print("=" * 60) - - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/proxy-docker-compose.yml b/proxy-docker-compose.yml new file mode 100644 index 0000000..f5b6183 --- /dev/null +++ b/proxy-docker-compose.yml @@ -0,0 +1,44 @@ +version: '3.8' + +services: + # Nginx Proxy Manager + npm: + image: jc21/nginx-proxy-manager:latest + container_name: tobiichiGPT-npm + restart: unless-stopped + ports: + - "10080:80" # HTTP + - "10443:443" # HTTPS + - "10081:81" # 管理介面 + volumes: + - npm-data:/data + - npm-letsencrypt:/etc/letsencrypt + environment: + - TZ=Asia/Taipei + networks: + - proxy-network + - tobiichiGPT-network + + # Cloudflare Tunnel + cloudflared: + image: cloudflare/cloudflared:latest + container_name: tobiichiGPT-cloudflared + restart: unless-stopped + command: tunnel run + environment: + - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} + networks: + - proxy-network + - tobiichiGPT-network + depends_on: + - npm + +networks: + proxy-network: + driver: bridge + tobiichiGPT-network: + external: true # 連接到主 stack 的網路 + +volumes: + npm-data: + npm-letsencrypt: