Rebuild
This commit is contained in:
122
QUICKSTART.md
122
QUICKSTART.md
@@ -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` 檔案)
|
|
||||||
374
README.md
374
README.md
@@ -5,161 +5,341 @@
|
|||||||
## 🎯 特色
|
## 🎯 特色
|
||||||
|
|
||||||
- ✅ **零修改** - 不需要修改 Open WebUI 程式碼
|
- ✅ **零修改** - 不需要修改 Open WebUI 程式碼
|
||||||
- ✅ **極簡化** - 單一 Python 檔案即可運行
|
- ✅ **極簡化** - 4 個容器完成所有功能
|
||||||
- ✅ **即插即用** - 偽裝成 OpenAI API,直接在 Open WebUI 設定中使用
|
- ✅ **共享資料庫** - Open WebUI 和管理後台使用同一個 PostgreSQL
|
||||||
- ✅ **視覺化後台** - 美觀的網頁介面供管理員回覆
|
- ✅ **視覺化後台** - NocoDB 提供專業的資料庫管理介面
|
||||||
|
- ✅ **可編輯代碼** - API 程式碼透過 bind mount 可直接修改
|
||||||
|
|
||||||
|
## 🏗️ 架構
|
||||||
|
|
||||||
|
### 主要服務(必須)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ PostgreSQL │ ← 共享資料庫
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────┴─────┬─────────────┬─────────────┐
|
||||||
|
│ │ │ │
|
||||||
|
┌───▼────┐ ┌──▼─────┐ ┌────▼─────┐ ┌───▼────┐
|
||||||
|
│ API │ │ Open │ │ NocoDB │ │ 用戶 │
|
||||||
|
│ 中間層 │ │ WebUI │ │ 管理介面 │ │ 瀏覽器 │
|
||||||
|
└────────┘ └────────┘ └──────────┘ └────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代理服務(選用)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────┐
|
||||||
|
│ 用戶 │
|
||||||
|
└─────┬─────┘
|
||||||
|
│ 公網
|
||||||
|
┌─────▼──────────────┐
|
||||||
|
│ Cloudflare Tunnel │
|
||||||
|
└─────┬──────────────┘
|
||||||
|
│ 內網
|
||||||
|
┌─────▼──────────────┐
|
||||||
|
│ Nginx Proxy Mgr │
|
||||||
|
└─────┬──────────────┘
|
||||||
|
│
|
||||||
|
┌─────▼──────────────┐
|
||||||
|
│ 主要服務 (上方) │
|
||||||
|
└────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
## 🚀 快速開始
|
## 🚀 快速開始
|
||||||
|
|
||||||
### 方法 1: Docker (推薦)
|
### 1. 準備環境檔案
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 進入 docker-stack 目錄
|
# 複製主服務環境變數
|
||||||
cd docker-stack
|
|
||||||
|
|
||||||
# 設定環境變數(首次使用)
|
|
||||||
Copy-Item .env.example .env
|
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 up -d
|
||||||
|
|
||||||
|
# 查看狀態
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
# 查看日誌
|
# 查看日誌
|
||||||
docker-compose logs -f
|
docker-compose logs -f
|
||||||
|
|
||||||
# 停止服務
|
|
||||||
docker-compose down
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方法 2: 直接執行
|
### 3. 啟動代理服務(選用)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 1. 進入 docker 目錄
|
# 啟動 Cloudflare Tunnel + NPM
|
||||||
cd docker
|
docker-compose -f docker-compose.proxy.yml --env-file .env.proxy up -d
|
||||||
|
|
||||||
# 2. 安裝依賴
|
# 查看狀態
|
||||||
pip install -r requirements.txt
|
docker-compose -f docker-compose.proxy.yml ps
|
||||||
|
|
||||||
# 3. 啟動伺服器
|
|
||||||
python human_reply_server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
啟動後會看到:
|
|
||||||
```
|
|
||||||
🚀 人工回覆伺服器啟動中...
|
|
||||||
📌 管理員後台: http://localhost:8000/admin
|
|
||||||
📌 API 端點: http://localhost:8000/v1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌐 服務訪問
|
## 🌐 服務訪問
|
||||||
|
|
||||||
啟動後可訪問以下服務:
|
### 主要服務
|
||||||
|
|
||||||
| 服務 | 網址 | 說明 |
|
| 服務 | 網址 | 說明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **Open WebUI** | http://localhost:3000 | 用戶對話介面 |
|
| **Open WebUI** | http://localhost:3000 | 用戶對話介面 |
|
||||||
| **管理員後台** | http://localhost:8000/admin | 真人回覆訊息 |
|
| **API 伺服器** | http://localhost:8000 | OpenAI API 相容端點 |
|
||||||
| **NPM 管理面板** | http://localhost:82 | Nginx Proxy Manager |
|
| **NocoDB** | http://localhost:8080 | 管理員回覆介面 |
|
||||||
| **Cloudflare Tunnel** | 自動連線 | 無需手動訪問 |
|
| **PostgreSQL** | localhost:5432 | 資料庫 |
|
||||||
|
|
||||||
預設帳號密碼(NPM):
|
### 代理服務(如已啟動)
|
||||||
|
|
||||||
|
| 服務 | 網址 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **NPM 管理面板** | http://localhost:81 | Nginx Proxy Manager |
|
||||||
|
| **HTTP 代理** | Port 80 | HTTP 流量 |
|
||||||
|
| **HTTPS 代理** | Port 443 | HTTPS 流量 |
|
||||||
|
|
||||||
|
**NPM 預設帳號**:
|
||||||
- Email: `admin@example.com`
|
- Email: `admin@example.com`
|
||||||
- Password: `changeme`
|
- Password: `changeme`
|
||||||
|
|
||||||
### 在 Open WebUI 中設定
|
## 📋 使用流程
|
||||||
|
|
||||||
|
### 設定 Open WebUI
|
||||||
|
|
||||||
1. 開啟 http://localhost:3000
|
1. 開啟 http://localhost:3000
|
||||||
2. 進入 **Settings** → **Connections**
|
2. 進入 **Settings** → **Connections**
|
||||||
3. 點擊 **+ Add OpenAI Connection**
|
3. 新增 OpenAI Connection:
|
||||||
4. 填入設定:
|
- **API Base URL**: `http://api:8000/v1`
|
||||||
- **API Base URL**: `http://human-reply-server:8000/v1`
|
- **API Key**: `sk-human` (任意值)
|
||||||
- **API Key**: 隨便填 (例如: `sk-human`)
|
4. 模型列表會出現 **human-admin**
|
||||||
5. 儲存後,模型列表會出現 **human-admin**
|
|
||||||
|
|
||||||
### 開始使用
|
### 管理員回覆
|
||||||
|
|
||||||
1. **用戶端**: 在 Open WebUI (http://localhost:3000) 選擇 `human-admin` 模型,發送訊息
|
1. 訪問 NocoDB: http://localhost:8080
|
||||||
2. **管理員**: 訪問 http://localhost:8000/admin,查看並回覆訊息
|
2. 連接到 PostgreSQL:
|
||||||
3. **自動刷新**: 後台每 5 秒自動刷新,顯示新訊息
|
- 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
|
```powershell
|
||||||
# 建立映像檔
|
docker-compose -f docker-compose.proxy.yml --env-file .env.proxy up -d
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 與 Open WebUI 整合
|
訪問你設定的網域即可從公網訪問服務。
|
||||||
|
|
||||||
如果 Open WebUI 也在 Docker 中運行,將兩者加入同一網路:
|
## 📦 資料持久化
|
||||||
|
|
||||||
|
所有服務資料都會持久化保存:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# 在 docker-compose.yml 中加入 Open WebUI
|
volumes:
|
||||||
version: '3.8'
|
postgres-data: # PostgreSQL 資料
|
||||||
|
openwebui-data: # Open WebUI 配置和對話記錄
|
||||||
|
nocodb-data: # NocoDB 配置
|
||||||
|
npm-data: # NPM 配置(代理服務)
|
||||||
|
npm-letsencrypt: # SSL 憑證(代理服務)
|
||||||
|
```
|
||||||
|
|
||||||
services:
|
## 🛠️ 修改 API 程式碼
|
||||||
human-reply-server:
|
|
||||||
|
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: .
|
build: .
|
||||||
container_name: tobiichi-gpt
|
container_name: tobiichi-gpt
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
fastapi==0.104.1
|
fastapi==0.104.1
|
||||||
uvicorn==0.24.0
|
uvicorn==0.24.0
|
||||||
pydantic==2.5.0
|
pydantic==2.5.0
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
asyncpg==0.29.0
|
||||||
207
api/server.py
Normal file
207
api/server.py
Normal file
@@ -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)
|
||||||
90
docker-compose.yml
Normal file
90
docker-compose.yml
Normal file
@@ -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:
|
||||||
@@ -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=
|
|
||||||
@@ -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:
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
__pycache__
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
.Python
|
|
||||||
*.so
|
|
||||||
*.egg
|
|
||||||
*.egg-info
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
*.md
|
|
||||||
LICENSE
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.log
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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"""
|
|
||||||
<div class="conversation" id="conv-{conv_id}">
|
|
||||||
<div class="conv-header">
|
|
||||||
<strong>對話 ID:</strong> {conv_id}
|
|
||||||
<span class="timestamp">{conv_data['timestamp']}</span>
|
|
||||||
</div>
|
|
||||||
<div class="user-message">
|
|
||||||
<strong>用戶:</strong> {conv_data['user_message']}
|
|
||||||
</div>
|
|
||||||
<textarea id="reply-{conv_id}" placeholder="輸入您的回覆..."></textarea>
|
|
||||||
<button onclick="sendReply('{conv_id}')">送出回覆</button>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
conversations_html = '<p class="no-messages">目前沒有待處理的訊息</p>'
|
|
||||||
|
|
||||||
html = f"""
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>管理員後台 - 人工回覆系統</title>
|
|
||||||
<style>
|
|
||||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
||||||
body {{
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 20px;
|
|
||||||
}}
|
|
||||||
.container {{
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
||||||
padding: 30px;
|
|
||||||
}}
|
|
||||||
h1 {{
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 28px;
|
|
||||||
}}
|
|
||||||
.subtitle {{
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-size: 14px;
|
|
||||||
}}
|
|
||||||
.conversation {{
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
animation: slideIn 0.3s ease;
|
|
||||||
}}
|
|
||||||
@keyframes slideIn {{
|
|
||||||
from {{ opacity: 0; transform: translateY(-10px); }}
|
|
||||||
to {{ opacity: 1; transform: translateY(0); }}
|
|
||||||
}}
|
|
||||||
.conv-header {{
|
|
||||||
margin-bottom: 15px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #dee2e6;
|
|
||||||
}}
|
|
||||||
.timestamp {{
|
|
||||||
float: right;
|
|
||||||
color: #6c757d;
|
|
||||||
font-size: 12px;
|
|
||||||
}}
|
|
||||||
.user-message {{
|
|
||||||
background: white;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
}}
|
|
||||||
textarea {{
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 2px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: inherit;
|
|
||||||
resize: vertical;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}}
|
|
||||||
textarea:focus {{
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}}
|
|
||||||
button {{
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 30px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 10px;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
}}
|
|
||||||
button:hover {{
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
|
||||||
}}
|
|
||||||
button:active {{
|
|
||||||
transform: translateY(0);
|
|
||||||
}}
|
|
||||||
.no-messages {{
|
|
||||||
text-align: center;
|
|
||||||
color: #6c757d;
|
|
||||||
padding: 40px;
|
|
||||||
font-style: italic;
|
|
||||||
}}
|
|
||||||
.status {{
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: white;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
||||||
display: none;
|
|
||||||
}}
|
|
||||||
.status.show {{
|
|
||||||
display: block;
|
|
||||||
animation: fadeIn 0.3s ease;
|
|
||||||
}}
|
|
||||||
@keyframes fadeIn {{
|
|
||||||
from {{ opacity: 0; }}
|
|
||||||
to {{ opacity: 1; }}
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="status" id="status"></div>
|
|
||||||
<div class="container">
|
|
||||||
<h1>🎯 管理員後台</h1>
|
|
||||||
<p class="subtitle">人工回覆系統 - 待處理訊息數量: {len(pending_conversations)}</p>
|
|
||||||
|
|
||||||
<div id="conversations">
|
|
||||||
{conversations_html}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function sendReply(convId) {{
|
|
||||||
const textarea = document.getElementById('reply-' + convId);
|
|
||||||
const reply = textarea.value.trim();
|
|
||||||
|
|
||||||
if (!reply) {{
|
|
||||||
alert('請輸入回覆內容');
|
|
||||||
return;
|
|
||||||
}}
|
|
||||||
|
|
||||||
fetch('/admin/reply', {{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {{ 'Content-Type': 'application/json' }},
|
|
||||||
body: JSON.stringify({{ conversation_id: convId, reply: reply }})
|
|
||||||
}})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {{
|
|
||||||
if (data.status === 'success') {{
|
|
||||||
showStatus('✅ 回覆已送出!');
|
|
||||||
document.getElementById('conv-' + convId).remove();
|
|
||||||
|
|
||||||
// 如果沒有待處理訊息,顯示提示
|
|
||||||
if (document.querySelectorAll('.conversation').length === 0) {{
|
|
||||||
document.getElementById('conversations').innerHTML =
|
|
||||||
'<p class="no-messages">目前沒有待處理的訊息</p>';
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
}})
|
|
||||||
.catch(error => {{
|
|
||||||
showStatus('❌ 送出失敗: ' + error);
|
|
||||||
}});
|
|
||||||
}}
|
|
||||||
|
|
||||||
function showStatus(message) {{
|
|
||||||
const status = document.getElementById('status');
|
|
||||||
status.textContent = message;
|
|
||||||
status.classList.add('show');
|
|
||||||
setTimeout(() => status.classList.remove('show'), 3000);
|
|
||||||
}}
|
|
||||||
|
|
||||||
// 每 5 秒自動重新整理
|
|
||||||
setInterval(() => location.reload(), 5000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</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)
|
|
||||||
44
proxy-docker-compose.yml
Normal file
44
proxy-docker-compose.yml
Normal file
@@ -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:
|
||||||
Reference in New Issue
Block a user