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 程式碼
|
||||
- ✅ **極簡化** - 單一 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:
|
||||
|
||||
@@ -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
|
||||
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