Update
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.env
|
||||||
|
*.log
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
cloudflared-config.yml
|
||||||
|
credentials.json
|
||||||
|
npm-data/
|
||||||
|
open-webui-data/
|
||||||
122
QUICKSTART.md
Normal file
122
QUICKSTART.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# 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` 檔案)
|
||||||
251
README.md
251
README.md
@@ -1,2 +1,251 @@
|
|||||||
# tobiichiGPT
|
# TobiichiGPT - 人工回覆系統
|
||||||
|
|
||||||
|
將 AI 對話系統改造成由真人管理員回覆的極簡方案。
|
||||||
|
|
||||||
|
## 🎯 特色
|
||||||
|
|
||||||
|
- ✅ **零修改** - 不需要修改 Open WebUI 程式碼
|
||||||
|
- ✅ **極簡化** - 單一 Python 檔案即可運行
|
||||||
|
- ✅ **即插即用** - 偽裝成 OpenAI API,直接在 Open WebUI 設定中使用
|
||||||
|
- ✅ **視覺化後台** - 美觀的網頁介面供管理員回覆
|
||||||
|
|
||||||
|
## 🚀 快速開始
|
||||||
|
|
||||||
|
### 方法 1: Docker (推薦)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 進入 docker-stack 目錄
|
||||||
|
cd docker-stack
|
||||||
|
|
||||||
|
# 設定環境變數(首次使用)
|
||||||
|
Copy-Item .env.example .env
|
||||||
|
notepad .env # 填入 Cloudflare Tunnel Token(若要使用)
|
||||||
|
|
||||||
|
# 使用 Docker Compose 啟動
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 查看日誌
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# 停止服務
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2: 直接執行
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. 進入 docker 目錄
|
||||||
|
cd docker
|
||||||
|
|
||||||
|
# 2. 安裝依賴
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 3. 啟動伺服器
|
||||||
|
python human_reply_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
啟動後會看到:
|
||||||
|
```
|
||||||
|
🚀 人工回覆伺服器啟動中...
|
||||||
|
📌 管理員後台: http://localhost:8000/admin
|
||||||
|
📌 API 端點: http://localhost:8000/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 服務訪問
|
||||||
|
|
||||||
|
啟動後可訪問以下服務:
|
||||||
|
|
||||||
|
| 服務 | 網址 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **Open WebUI** | http://localhost:3000 | 用戶對話介面 |
|
||||||
|
| **管理員後台** | http://localhost:8000/admin | 真人回覆訊息 |
|
||||||
|
| **NPM 管理面板** | http://localhost:82 | Nginx Proxy Manager |
|
||||||
|
| **Cloudflare Tunnel** | 自動連線 | 無需手動訪問 |
|
||||||
|
|
||||||
|
預設帳號密碼(NPM):
|
||||||
|
- Email: `admin@example.com`
|
||||||
|
- Password: `changeme`
|
||||||
|
|
||||||
|
### 在 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 (http://localhost:3000) 選擇 `human-admin` 模型,發送訊息
|
||||||
|
2. **管理員**: 訪問 http://localhost:8000/admin,查看並回覆訊息
|
||||||
|
3. **自動刷新**: 後台每 5 秒自動刷新,顯示新訊息
|
||||||
|
|
||||||
|
## 📋 運作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用戶發送訊息
|
||||||
|
↓
|
||||||
|
Open WebUI 調用 API (轉圈圈等待)
|
||||||
|
↓
|
||||||
|
訊息進入待處理隊列
|
||||||
|
↓
|
||||||
|
管理員在後台看到訊息
|
||||||
|
↓
|
||||||
|
管理員輸入並送出回覆
|
||||||
|
↓
|
||||||
|
API 回傳給 Open WebUI
|
||||||
|
↓
|
||||||
|
用戶收到回覆
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 技術架構
|
||||||
|
|
||||||
|
- **FastAPI**: 輕量級 Python Web 框架
|
||||||
|
- **AsyncIO**: 異步等待管理員回覆
|
||||||
|
- **偽裝 OpenAI API**:
|
||||||
|
- `/v1/models` - 模型列表
|
||||||
|
- `/v1/chat/completions` - 聊天完成端點
|
||||||
|
|
||||||
|
## 🐳 Docker 部署
|
||||||
|
|
||||||
|
### 檔案結構
|
||||||
|
```
|
||||||
|
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 指令
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 與 Open WebUI 整合
|
||||||
|
|
||||||
|
如果 Open WebUI 也在 Docker 中運行,將兩者加入同一網路:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 在 docker-compose.yml 中加入 Open WebUI
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
human-reply-server:
|
||||||
|
build: .
|
||||||
|
container_name: tobiichi-gpt
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
networks:
|
||||||
|
- tobiichi-network
|
||||||
|
|
||||||
|
open-webui:
|
||||||
|
image: ghcr.io/open-webui/open-webui:main
|
||||||
|
container_name: open-webui
|
||||||
|
ports:
|
||||||
|
- "3000:8080"
|
||||||
|
volumes:
|
||||||
|
- open-webui:/app/backend/data
|
||||||
|
networks:
|
||||||
|
- tobiichi-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tobiichi-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
open-webui:
|
||||||
|
```
|
||||||
|
|
||||||
|
然後在 Open WebUI 中使用:
|
||||||
|
- **API Base URL**: `http://human-reply-server:8000/v1`
|
||||||
|
|
||||||
|
## 📝 進階設定
|
||||||
|
|
||||||
|
### 修改等待超時時間
|
||||||
|
|
||||||
|
編輯 `docker/human_reply_server.py` 第 79 行:
|
||||||
|
|
||||||
|
```python
|
||||||
|
timeout = 600 # 預設 10 分鐘,可改為其他秒數
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改伺服器埠號
|
||||||
|
|
||||||
|
**方法 1: 修改 docker-compose.yml**
|
||||||
|
```yaml
|
||||||
|
# 編輯 docker-stack/docker-compose.yml
|
||||||
|
ports:
|
||||||
|
- "9000:8000" # 本機 9000 對應容器 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
**方法 2: 修改程式碼**
|
||||||
|
```python
|
||||||
|
# 編輯 docker/human_reply_server.py
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000) # 改為其他埠號
|
||||||
|
```
|
||||||
|
|
||||||
|
### 設定 Cloudflare Tunnel
|
||||||
|
|
||||||
|
詳細設定步驟請參考專案根目錄的說明文件,或參考 `docker-stack/.env.example`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd docker-stack
|
||||||
|
Copy-Item .env.example .env
|
||||||
|
notepad .env # 填入您的 CLOUDFLARE_TUNNEL_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 支援多管理員
|
||||||
|
|
||||||
|
目前版本採「先搶先贏」機制,任何管理員都可以回覆任何訊息。若需要分配機制,可以加入:
|
||||||
|
- 管理員登入系統
|
||||||
|
- 訊息認領機制
|
||||||
|
- 管理員負載平衡
|
||||||
|
|
||||||
|
## ⚠️ 注意事項
|
||||||
|
|
||||||
|
- 此為**最簡化版本**,適合小規模使用
|
||||||
|
- 訊息儲存在記憶體中,重啟會遺失
|
||||||
|
- 沒有身份驗證,建議僅在內網使用
|
||||||
|
- 若需生產環境,建議加入:
|
||||||
|
- 資料庫持久化
|
||||||
|
- 身份驗證
|
||||||
|
- WebSocket 即時推送
|
||||||
|
- 訊息隊列系統
|
||||||
|
|
||||||
|
## 🎨 管理員後台截圖
|
||||||
|
|
||||||
|
後台提供:
|
||||||
|
- 待處理訊息列表
|
||||||
|
- 用戶訊息顯示
|
||||||
|
- 回覆文字框
|
||||||
|
- 一鍵送出功能
|
||||||
|
- 自動刷新
|
||||||
|
|||||||
4
docker-stack/.env.example
Normal file
4
docker-stack/.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Cloudflare Tunnel Token
|
||||||
|
# 請到 Cloudflare Zero Trust Dashboard 建立 Tunnel 並取得 Token
|
||||||
|
# https://one.dash.cloudflare.com/
|
||||||
|
CLOUDFLARE_TUNNEL_TOKEN=your-tunnel-token-here
|
||||||
69
docker-stack/docker-compose.yml
Normal file
69
docker-stack/docker-compose.yml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# 人工回覆伺服器
|
||||||
|
human-reply-server:
|
||||||
|
build: .
|
||||||
|
container_name: tobiichi-gpt
|
||||||
|
ports:
|
||||||
|
- "8000: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:
|
||||||
|
- "3000: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:
|
||||||
|
- "8080:80" # HTTP (避免與現有 NPM 衝突)
|
||||||
|
- "8443:443" # HTTPS (避免與現有 NPM 衝突)
|
||||||
|
- "82: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:
|
||||||
17
docker/.dockerignore
Normal file
17
docker/.dockerignore
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
18
docker/Dockerfile
Normal file
18
docker/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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"]
|
||||||
384
docker/human_reply_server.py
Normal file
384
docker/human_reply_server.py
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
"""
|
||||||
|
人工回覆伺服器 - 偽裝成 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)
|
||||||
3
docker/requirements.txt
Normal file
3
docker/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn==0.24.0
|
||||||
|
pydantic==2.5.0
|
||||||
Reference in New Issue
Block a user